在互联网大厂算法面试中,链表题型的出现频率常年稳居前 5,而快慢指针作为解决链表问题的 “万能钥匙”,更是高频考点中的重中之重。根据牛客网 2025 年 Q2《算法面试趋势报告》显示,在字节跳动、阿里、腾讯等大厂的链表面试题中,涉及快慢指针的题目占比高达 68%,其中 “判断链表有环”“寻找链表中点” 等题型的重复考察率超过 40%。但不少面试者因对原理理解不深、代码细节把控不足,常常在这类基础题上栽跟头。今天这篇文章,就带大家彻底吃透快慢指针的核心逻辑,从概念到实现,从真题到避坑,帮你轻松应对大厂面试中的链表考点。
为什么快慢指针是链表问题的 “最优解”?
在聊具体用法前,我们先搞懂一个关键问题:为什么解决链表问题非要用快慢指针?这还要从链表的存储特性说起。
链表不同于数组,它的节点通过指针串联,无法像数组那样通过下标直接访问元素。如果想用常规方法解决 “找中点”“找倒数第 K 个节点” 这类问题,通常需要先遍历一次链表统计长度,再遍历第二次定位目标节点 —— 这种 “两次遍历” 的方案时间复杂度虽也是 O (n),但在面试中会被认为 “不够优雅”,而快慢指针能实现一次遍历完成目标,更符合大厂对算法效率与代码简洁性的要求。
所谓快慢指针,本质是定义两个起始位置相同的指针,通过设置不同的移动步长(通常慢指针一次 1 步,快指针一次 2 步),利用两者的 “速度差” 实现对链表的高效遍历。这种思路不仅适用于单链表,在双向链表、循环链表中也能灵活运用,是算法面试中的 “性价比之王” 技巧。
快慢指针 3 大核心用法:原理 + 代码 + 场景全解析
(一)用法 1:判断链表是否有环(高频中的高频)
1. 问题场景
给定一个单链表,判断链表中是否存在环。例如:链表 A→B→C→B,其中 C 的 next 指向 B,形成一个环,此时需返回 true;若链表为 A→B→C→null,则返回 false。
2. 原理分析
这是快慢指针最经典的应用,核心逻辑类似 “追及问题”:若链表无环,快指针会先到达链表尾部(指向 null);若链表有环,快指针会在环内循环移动,最终追上慢指针(两者指向同一个节点)。这里要注意两个关键细节:
- 快指针的移动步长必须是慢指针的 2 倍吗?其实不一定,只要快指针比慢指针快即可(如快指针 3 步、慢指针 1 步),但 2 倍步长是最常用、代码最简洁的方案。
- 循环终止条件是什么?必须先判断fast != null和fast.next != null,再移动指针,避免出现空指针异常(NPE)。
3. 完整代码实现(Java 版)
// 定义链表节点类
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
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; // 慢指针走1步
fast = fast.next.next; // 快指针走2步
// 若相遇,说明有环
if (slow == fast) {
return true;
}
}
// 循环结束仍未相遇,无环
return false;
}
// 测试案例
public static void main(String[] args) {
LinkedListCycle solution = new LinkedListCycle();
// 案例1:有环链表
ListNode head1 = new ListNode(3);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(0);
ListNode node4 = new ListNode(-4);
head1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2; // 形成环:-4→2
System.out.println("案例1(有环):" + solution.hasCycle(head1)); // 输出true
// 案例2:无环链表
ListNode head2 = new ListNode(1);
head2.next = new ListNode(2);
System.out.println("案例2(无环):" + solution.hasCycle(head2)); // 输出false
}
}4. 面试延伸问题
大厂面试中常在此基础上追问:“若链表有环,如何找到环的入口节点?” 解法是:当快慢指针相遇后,将慢指针重置为头节点,然后两者以相同步长(每次 1 步)移动,再次相遇的节点即为环的入口。
(二)用法 2:寻找链表的中点(二分法基础)
1. 问题场景
给定一个单链表,返回链表的中间节点。若链表长度为偶数,返回第二个中间节点(如链表 1→2→3→4,返回 3);若为奇数,返回正中间节点(如 1→2→3,返回 2)。
2. 原理分析
利用快慢指针的速度差,当快指针到达链表尾部时,慢指针恰好停在中点位置。这里的关键是 “快指针的终止位置”:
- 若快指针移动到fast.next == null(奇数长度),此时慢指针指向正中间节点。
- 若快指针移动到fast == null(偶数长度),此时慢指针指向第二个中间节点。
这种特性使其成为 “链表二分法” 的基础,例如在 “链表归并排序”“判断回文链表” 等问题中都有重要应用。
3. 完整代码实现(Java 版)
public class MiddleNode {
public ListNode middleNode(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode slow = head;
ListNode fast = head;
// 循环条件:快指针未到达尾部
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 循环结束,slow即为中点
return slow;
}
// 测试案例
public static void main(String[] args) {
MiddleNode solution = new MiddleNode();
// 案例1:奇数长度链表(1→2→3→4→5)
ListNode head1 = new ListNode(1);
head1.next = new ListNode(2);
head1.next.next = new ListNode(3);
head1.next.next.next = new ListNode(4);
head1.next.next.next.next = new ListNode(5);
System.out.println("案例1中点值:" + solution.middleNode(head1).val); // 输出3
// 案例2:偶数长度链表(1→2→3→4)
ListNode head2 = new ListNode(1);
head2.next = new ListNode(2);
head2.next.next = new ListNode(3);
head2.next.next.next = new ListNode(4);
System.out.println("案例2中点值:" + solution.middleNode(head2).val); // 输出3
}
}(三)用法 3:寻找链表的倒数第 K 个节点(技巧性拉满)
1. 问题场景
给定一个单链表,找出链表的倒数第 K 个节点。例如:链表 1→2→3→4→5,倒数第 2 个节点为 4;若 K=5,则返回 1。
2. 原理分析
常规思路是 “先遍历统计长度,再遍历定位”,但快慢指针能实现 “一次遍历”:
- 让快指针先向前移动 K 步,此时快慢指针之间间隔 K 个节点。
- 然后快慢指针同时以 1 步 / 次的速度移动,直到快指针到达链表尾部(指向 null)。
- 此时慢指针指向的节点即为倒数第 K 个节点。
这里需要特别注意边界条件处理:当 K 大于链表长度时,应返回 null;当 K=0 或负数时,也需返回 null(符合实际业务逻辑)。
3. 完整代码实现(Java 版)
public class KthFromEnd {
public ListNode getKthFromEnd(ListNode head, int k) {
// 边界条件:空链表或K≤0,返回null
if (head == null || k <= 0) {
return null;
}
ListNode slow = head;
ListNode fast = head;
// 第一步:快指针先移动K步
for (int i = 0; i < k; i++) {
// 若快指针提前到达null,说明K大于链表长度
if (fast == null) {
return null;
}
fast = fast.next;
}
// 第二步:快慢指针同时移动,直到快指针为null
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
// 慢指针即为倒数第K个节点
return slow;
}
// 测试案例
public static void main(String[] args) {
KthFromEnd solution = new KthFromEnd();
// 构建链表:1→2→3→4→5
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
// 案例1:倒数第2个节点
System.out.println("案例1(倒数第2个):" + solution.getKthFromEnd(head, 2).val); // 输出4
// 案例2:倒数第5个节点
System.out.println("案例2(倒数第5个):" + solution.getKthFromEnd(head, 5).val); // 输出1
// 案例3:K=6(大于链表长度)
System.out.println("案例3(K=6):" + solution.getKthFromEnd(head, 6)); // 输出null
}
}5 道大厂面试真题实战:从基础到进阶
掌握了核心用法后,我们通过 5 道大厂真题巩固练习,这些题目均来自 2025 年字节、阿里、美团的真实面试场景。
真题 1:判断链表是否有环(字节跳动 - 初级)
题目:给你一个链表的头节点 head,判断链表中是否有环。(同用法 1,此处略去代码,重点看面试中的易错点)
易错点:
- 忘记处理空链表或单节点链表的边界条件。
- 循环条件写成while (fast.next != null && fast != null),会导致空指针异常(当 fast 为 null 时,fast.next 会报错)。
- 快慢指针初始化时,让 fast 直接指向 head.next,导致判断逻辑偏差。
真题 2:环形链表 II(阿里 - 中级)
题目:给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,则返回 null。
解法思路:
- 先用快慢指针判断链表是否有环,若无环返回 null。
- 若有环,将慢指针重置为 head,快指针保持在相遇点。
- 两者以 1 步 / 次的速度移动,再次相遇的节点即为环的入口。
代码核心片段:
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
ListNode slow = head, fast = head;
// 第一步:判断是否有环
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) break;
}
// 若无环,返回null
if (fast == null || fast.next == null) return null;
// 第二步:找环入口
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}真题 3:回文链表(腾讯 - 中级)
题目:给你一个单链表的头节点 head,请你判断该链表是否为回文链表。
解法思路:
- 用快慢指针找到链表中点。
- 反转中点后的后半部分链表。
- 对比前半部分与反转后的后半部分,若全部相同则为回文。
关键步骤:反转后半部分链表的代码实现。
真题 4:删除链表的倒数第 N 个节点(美团 - 中级)
题目:给你一个链表,删除链表的倒数第 n 个节点,并且返回链表的头节点。
解法思路:
- 用快慢指针找到倒数第 n 个节点的前一个节点(避免删除头节点时的边界问题)。
- 改变前一个节点的 next 指针,跳过倒数第 n 个节点。
技巧:可创建一个 “虚拟头节点”(dummy node),让慢指针初始指向 dummy,避免处理头节点删除的特殊情况。
真题 5:链表的中间节点(百度 - 初级)
题目:同用法 2,此处重点考察代码的简洁性与边界处理能力。
面试加分项:能主动说明 “若需返回第一个中间节点(偶数长度时),如何修改代码”(将循环条件改为while (fast.next != null && fast.next.next != null))。
避坑指南:新手常犯的 4 个错误
边界条件遗漏:忘记处理空链表、单节点链表、K 大于链表长度等情况,导致代码在特殊测试用例下崩溃。
指针移动顺序错误:在判断相遇前先移动指针,或移动顺序颠倒,导致逻辑错误。
空指针异常:循环条件未先判断fast != null就访问fast.next,尤其在偶数长度链表中容易出现。
步长设置不当:在判断环的问题中,将快指针步长设为 3 步及以上,虽然理论可行,但会增加代码复杂度,且容易出现 “跳过相遇点” 的情况,面试中建议优先用 2 倍步长。
总结
吃透原理:记住 “速度差” 是快慢指针的核心,不同用法的本质是利用速度差实现 “一次遍历定位目标”。
多写代码:将 3 大核心用法的代码手写 3-5 遍,确保能脱离参考独立完成,重点关注边界条件处理。
真题实战:完成本文中的 5 道真题后,可在 LeetCode 上搜索 “链表”“快慢指针” 标签的题目(推荐题号:141、142、876、19、234),强化实战能力。
算法面试拼的不是天赋,而是方法与练习。快慢指针作为入门级的技巧,只要掌握原理、多练多总结,就能成为你面试中的 “得分利器”。最后留一个小问题:如何用快慢指针判断两个单链表是否相交?欢迎在评论区留下你的思路,点赞过 500,下期我们详细讲解!
