大厂面试刷题必备:从海量面经精挑的大厂高频手撕面试题(Java版本实现)

第1章:链表的那些“相交”与“反转”戏码

1.1 判断两个链表是否相交(字节跳动、货拉拉)

问题背景

在面试中,判断两个单链表是否相交是个经典题目,常被字节、货拉拉等公司拿来考察你的链表操作能力。相交的意思是,两个链表在某个节点开始共享相同的节点。难点在于如何高效判断,并且还能优化性能。

思路拆解

  • 双指针法(优雅且高效):想象两个链表像两条绳子,可能在某处“打了个结”。我们可以通过计算长度差,让长链表先“走几步”,然后同步遍历,判断是否相遇。这种方法直观,空间复杂度低。步骤:遍历两个链表,分别计算长度 lenA 和 lenB。让长链表的指针先走 |lenA - lenB| 步,抹平长度差。两个指针同步前进,比较是否指向同一节点。如果走到末尾(null)还没相遇,说明不相交。
  • 哈希表法(快速但占空间):如果你追求速度,可以用哈希表存储一个链表的节点,遍历另一个链表时检查节点是否在哈希表中。找到即相交,没找到就不相交。注意:哈希表法虽然查找快,但需要额外空间,面试时要和面试官确认空间复杂度要求!

代码实现(双指针法)

class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public class LinkedListIntersection {
    public boolean isIntersect(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return false;

        // 计算长度
        int lenA = getLength(headA);
        int lenB = getLength(headB);

        // 抹平长度差
        while (lenA > lenB) {
            headA = headA.next;
            lenA--;
        }
        while (lenB > lenA) {
            headB = headB.next;
            lenB--;
        }

        // 同步遍历,检查是否相交
        while (headA != null && headB != null) {
            if (headA == headB) return true;
            headA = headA.next;
            headB = headB.next;
        }
        return false;
    }

    private int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            length++;
            head = head.next;
        }
        return length;
    }

    public static void main(String[] args) {
        ListNode headA = new ListNode(1);
        headA.next = new ListNode(2);
        headA.next.next = new ListNode(3);
        ListNode intersect = new ListNode(4);
        headA.next.next.next = intersect;
        headA.next.next.next.next = new ListNode(5);

        ListNode headB = new ListNode(6);
        headB.next = new ListNode(7);
        headB.next.next = intersect;

        LinkedListIntersection solution = new LinkedListIntersection();
        System.out.println("相交? " + solution.isIntersect(headA, headB)); // 输出 true
    }
}

优化技巧

  • 双指针的巧妙变体:如果不想单独算长度,可以用“环形追逐”思路:两个指针分别从 headA 和 headB 出发,遍历到末尾后切换到另一个链表继续走。如果相交,指针总会相遇;如果不相交,指针会同时到达 null。时间复杂度仍为 O(m+n),但代码更简洁。
  • 哈希表优化:如果用哈希表,优先用 HashSet,因为它只存储节点引用,空间复杂度为 O(min(m,n))。注意,哈希表法在链表很长时可能更快,但内存占用要权衡。

时间复杂度:双指针法 O(m+n),哈希表法平均 O(m+n),最坏 O(m+n)。

空间复杂度:双指针法 O(1),哈希表法 O(min(m,n))。

面试Tips

  • 面试官可能追问:如果链表有环怎么办?双指针法依然适用,但哈希表法需要小心处理环的情况。
  • 实际编码时,记得处理空链表的边界条件,避免空指针异常!

1.2 单链表反转(贝壳)

问题背景

单链表反转是大厂面试的“常驻嘉宾”,贝壳、字节等公司尤其爱考。要求在 O(1) 空间复杂度下,将链表的指向全部反转,比如 1->2->3 变成 3->2->1。

思路拆解

  • 迭代法(直观且节省空间):用三个指针:prev(前节点)、curr(当前节点)、next(下一节点)。每次让 curr.next 指向 prev,然后三指针一起后移。步骤:初始化 prev = null,curr = head。保存 next = curr.next。将 curr.next 指向 prev。移动 prev = curr,curr = next,继续循环。
  • 递归法(优雅但需栈空间):递归到链表末尾,从最后一个节点开始反转指向,逐步回溯调整指针。

代码实现(迭代法)

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
}

public class ReverseLinkedList {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;

        while (curr != null) {
            ListNode next = curr.next; // 保存下一节点
            curr.next = prev;         // 反转指针
            prev = curr;              // 前节点后移
            curr = next;              // 当前节点后移
        }
        return prev; // 新头节点
    }

    public void printList(ListNode head) {
        while (head != null) {
            System.out.print(head.val + " -> ");
            head = head.next;
        }
        System.out.println("null");
    }

    public static void main(String[] args) {
        ReverseLinkedList solution = new ReverseLinkedList();
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);

        System.out.println("原链表:");
        solution.printList(head);

        ListNode reversed = solution.reverseList(head);
        System.out.println("反转后:");
        solution.printList(reversed);
    }
}

优化技巧

  • 边界处理:空链表或单节点链表直接返回,减少不必要循环。
  • 递归写法:如果面试官要求递归,可以展示递归解法,但要说明递归会用 O(n) 栈空间,不如迭代法省空间。
  • 扩展问题:面试官可能问“反转前K个节点”或“反转指定区间”,可以用类似逻辑,记录反转段的前后节点,重新连接。

时间复杂度:O(n),遍历一次链表。

空间复杂度:O(1)(迭代法),O(n)(递归法)。

面试Tips

  • 写代码时,画个简单的链表图,手动推演指针变化,能避免逻辑错误。
  • 如果面试官要求返回新头节点,记得返回 prev,因为它指向反转后的头!

1.3 链表判环与找入口(新浪)

问题背景

链表判环和找环入口是新浪、字节等公司常考的进阶题。不仅要判断链表是否有环,还要精确找到环的入口节点,考察你对快慢指针的掌握。

思路拆解

  • 判环(快慢指针):用两个指针,慢指针走一步,快指针走两步。如果链表有环,它们一定会相遇;如果无环,快指针会先到 null。
  • 找入口:快慢指针相遇后,将慢指针重置到头节点,然后两者以相同速度(一步)移动,再次相遇的节点就是环入口。原理:假设头到入口距离为 a,入口到相遇点距离为 b,环剩余部分为 c。快慢指针相遇时,慢指针走了 a+b,快指针走了 2(a+b)。通过数学推导,第二次相遇必在入口。

代码实现

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
}

public class LinkedListCycle {
    // 判断是否有环
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) return false;

        ListNode slow = head;
        ListNode fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) return true;
        }
        return false;
    }

    // 找环入口
    public ListNode findCycleEntry(ListNode head) {
        if (head == null || head.next == null) return null;

        ListNode slow = head, fast = head;
        boolean hasCycle = false;

        // 快慢指针找相遇点
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                hasCycle = true;
                break;
            }
        }

        if (!hasCycle) return null;

        // 慢指针回起点,同步走,找入口
        slow = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }

    public static void main(String[] args) {
        LinkedListCycle solution = new LinkedListCycle();
        ListNode node1 = new ListNode(1);
        ListNode node2 = new ListNode(2);
        ListNode node3 = new ListNode(3);
        ListNode node4 = new ListNode(4);
        node1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node2; // 环入口为node2

        System.out.println("有环? " + solution.hasCycle(node1)); // true
        ListNode entry = solution.findCycleEntry(node1);
        System.out.println("环入口值:" + (entry != null ? entry.val : "无环")); // 2
    }
}

优化技巧

  • 边界检查:优先判断空链表或单节点,节省时间。
  • 数学推导:面试时如果被问到原理,简单解释快慢指针的数学关系(2(a+b) = a+b+c+b),能加分。
  • 扩展问题:如果要求返回环长度,可以在快慢指针相遇后继续走,统计循环次数。

时间复杂度:O(n),快慢指针遍历一次,找入口再遍历一次。

空间复杂度:O(1),仅用常数空间。

面试Tips

  • 画图是王道!用图展示快慢指针的移动和相遇,能让面试官直观理解你的思路。
  • 如果链表超长,面试官可能问如何优化,提示:快慢指针已经是最佳方案!

1.4 合并两个有序链表(字节跳动、货拉拉)

问题背景

合并两个有序链表是链表操作的经典题,字节、货拉拉常考。要求将两个升序链表合并成一个升序链表,考察指针操作。

思路拆解

  • 核心逻辑:用双指针比较链表节点,构建新链表。步骤:创建虚拟头节点(dummy)简化边界处理。比较两链表当前节点,取较小者接到结果链表,移动指针。处理剩余节点,直接接到结果尾部。

代码实现

class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public class MergeSortedListNodes {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;

        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                current.next = l1;
                l1 = l1.next;
            } else {
                current.next = l2;
                l2 = l2.next;
            }
            current = current.next;
        }
        current.next = l1 != null ? l1 : l2;

        return dummy.next;
    }

    public void printList(ListNode head) {
        while (head != null) {
            System.out.print(head.val + " ");
            head = head.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        MergeSortedListNodes solution = new MergeSortedListNodes();
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(2);
        l1.next.next = new ListNode(4);
        ListNode l2 = new ListNode(1);
        l2.next = new ListNode(3);
        l2.next.next = new ListNode(4);

        System.out.print("合并后:");
        ListNode merged = solution.mergeTwoLists(l1, l2);
        solution.printList(merged);
    }
}

优化技巧

  • 虚拟头节点:用dummy简化代码,避免单独处理头节点。
  • 递归写法:可用递归合并,代码更简洁但栈空间为 O(n+m)。
  • 原地合并:若允许修改原链表,可直接调整指针,节省空间。

时间复杂度:O(n + m),遍历两链表。

空间复杂度:O(1)(迭代法),递归法为 O(n+m)。

面试Tips

  • 画出链表合并过程,标注dummy和current指针,能直观展示逻辑。
  • 如果面试官问递归解法,展示后说明空间复杂度差异。

第2章:排序算法的“手撕”盛宴

2.1 冒泡排序(美团)

问题背景

冒泡排序简单粗暴,是美团等公司考察基本功的常考题。虽然实际开发中很少用,但它能检验你对循环和交换逻辑的熟练度。

思路拆解

  • 核心逻辑:像气泡一样,把最大(或最小)元素逐步“冒”到数组一端。步骤:外层循环控制轮数,每轮确定一个最大元素。内层循环比较相邻元素,若前者大于后者,交换。优化:如果某轮没发生交换,说明数组已有序,提前退出。

代码实现

public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            boolean swapped = false;
            for (int j = 0; j < n - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true;
                }
            }
            if (!swapped) break; // 提前退出
        }
    }

    public static void main(String[] args) {
        int[] arr = {5, 2, 9, 1, 5, 6};
        System.out.print("排序前:");
        for (int num : arr) System.out.print(num + " ");
        bubbleSort(arr);
        System.out.print("\n排序后:");
        for (int num : arr) System.out.print(num + " ");
    }
}

优化技巧

  • 提前终止:swapped 标记是关键,能将最好情况优化到 O(n)。
  • 双向冒泡:面试官可能问“鸡尾酒排序”(双向冒泡),可以正反两方向冒泡,减少轮次。

时间复杂度:最好 O(n),最坏/平均 O(n²)。

空间复杂度:O(1),原地排序。

面试Tips

  • 写冒泡排序时,注意循环边界,避免越界。
  • 如果面试官要求优化,提到 swapped 标志和双向冒泡,能展现你的深度!

2.2 快速排序(作业帮)

问题背景

快速排序(Quick Sort)是大厂面试的“宠儿”,作业帮、字节等公司常考。它基于分治法,速度快,效率高,但实现细节容易出错,面试官爱看你能否写得又快又准。

思路拆解

  • 核心逻辑:选一个基准(pivot),将数组分成“小于pivot”和“大于pivot”两部分,递归处理子数组。步骤:选择pivot(常见选择:首元素、末元素、随机或中间值)。分区(partition):通过双指针调整数组,让小于pivot的元素在左边,大于pivot的在右边,pivot归位。递归排序左右子数组。
  • 关键点:分区操作要高效,pivot选择影响性能(最坏情况是数组已排序,退化到O(n²))。

代码实现

public class QuickSort {
    public static void quickSort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        quickSortHelper(arr, 0, arr.length - 1);
    }

    private static void quickSortHelper(int[] arr, int left, int right) {
        if (left < right) {
            int pivotIndex = partition(arr, left, right);
            quickSortHelper(arr, left, pivotIndex - 1);
            quickSortHelper(arr, pivotIndex + 1, right);
        }
    }

    private static int partition(int[] arr, int left, int right) {
        int pivot = arr[right]; 

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

17年+码农经历了很多次面试,多次作为面试官面试别人,多次大数据面试和面试别人,深知哪些面试题是会被经常问到。 在多家企业从0到1开发过离线数仓实时数仓等多个大型项目,详细介绍项目架构等企业内部秘不外传的资料,介绍踩过的坑和开发干货,分享多个拿来即用的大数据ETL工具,让小白用户快速入门并精通,指导如何入职后快速上手。 计划更新内容100篇以上,包括一些企业内部秘不外宣的干货,欢迎订阅!

全部评论
写的很认真
点赞 回复 分享
发布于 07-26 14:33 香港

相关推荐

07-25 05:28
深度学习
现在是搭建了一个AI生态,唉唉,生态搭建完了,就是少少也有1000台计算机吧gpucpu大量的储存芯片和大量处理锌芯片,构建一个i体系AI体系连接的是电脑手机,通过电脑和手机连接大型大型计算机体系,通过发送信息,信息接收转基站,基站转入计算机那里就是直接信息转入基站,基站通过问题分析,信西分析,正确的信息发送,发送到手机,之后就是计入AI生态计算机分析出来的正确的答案去提高工作效率,现在的全世界,用AI去完全替代然后就是通过AI体系,其负责在人类工作,生活和学习上和医疗金融工作,本质就是用AI的大数据模型去工作,提高工作效率,现在全世界的AI专家呀,无非就是填补AI的短板,就是AI不知道的东西,用写代码敲代码搞编程去输入正确的答案给数据库,然后数据库用正确的答案去回答人们的那个问题的答案,就是说现在AI体系搭建完毕了,现在就是说要不断的AI人才去填补那个AI信息的欠缺和那个坑,我只知道AI体系生态AI模型生态的底层逻辑呃,这我都没学过的,这是我大脑想出来的我跟你说了这个东西,你不应该拍一下我马屁吗?我没学过,我都知道这个原理,慢慢填坑AI生态AI数据,慢慢去编写写代码去助推助推计算机体系和添加数据库模型,去改善AI数据库跑的更好用,用的更好,这就是AI世界AI生态的底层逻辑
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务