环形链表 II 图解题解
快慢指针能判断链表有没有环,但环从哪里开始——这才是真正的难题。
操场上两个人跑圈,跑得快的那个总会追上慢的——相遇证明有环。找到相遇点之后有个数学结论:让一个人回到起跑线,两人再以同样速度走,再次相遇的地方就是圈的入口。不用记路,不用额外做标记,两次相遇就把入口精确锁定,全程 O(1) 空间。
这道题到底在问什么
- 输入
- head=[6,3,7,2,…], 尾→值2那个节点
- 输出
- 值为 2 的节点(环入口)
最优解:为什么这么做
一句话答案:LeetCode 142 环形链表 II 的最优解是快慢指针两阶段:slow 每次走 1 步、fast 走 2 步,有环则必在环内相遇;再让一个指针从链表头出发与 slow 同速前进,两者再次相遇的位置就是环入口。O(n) 时间、O(1) 空间,比哈希集合记录访问节点省掉 O(n) 内存。
这道题比判断有环多问了什么
LeetCode 141 只问链表里有没有环,快慢指针一相遇就能返回 true。本题要的更多:把环的入口节点找出来,也就是链表从哪个节点开始绕圈;若无环则返回 null。直观的做法是哈希集合——边走边把访问过的节点存进集合,第一个重复见到的节点就是入口,时间 O(n) 但空间也是 O(n)。进阶目标是不开任何额外结构,只靠几个指针把入口逼出来。
快慢指针为什么一定相遇,不会互相跳过
slow 每次走一步、fast 每次走两步。只要有环,两个指针迟早都会进环,此后每一轮 fast 与 slow 的距离恰好缩短一步——一步一步逼近,不存在「跨过去错过了」的可能,所以必然在环内某个位置相遇。若无环,fast 会先撞到 null,循环自然结束、返回 null。
要强调的是,这次相遇只证明环存在,相遇点一般并不是环入口——fast 是在环里绕圈追上 slow 的,追上的位置取决于环的形状。把相遇点直接当答案返回,是这道题最常见的错误。
为什么从头和相遇点同速走,会在入口碰头
设链表头到环入口的距离为 a,入口沿环走到相遇点的距离为 b,环长为 c。相遇那一刻 fast 的路程是 slow 的两倍:slow 走了 a+b,fast 走了 a+b 再加若干整圈。把两倍关系展开消项,得到 a 等于 c-b 加上若干整圈。
翻译成白话:从链表头再走 a 步会到达入口;而从相遇点出发走同样的 a 步,等于先补完这一圈剩下的 c-b 步回到入口、再原地绕几个整圈,落点还是入口。所以让一个新指针从头出发、slow 留在相遇点,两者同速各走一步,第一次相遇必然发生在环入口,返回那个节点即可。
复杂度与三个容易写错的地方
阶段一最多走 O(n) 步就会相遇或出局,阶段二再走头到入口的距离,总时间 O(n);始终只用两三个指针,空间 O(1)。
易错点:一是循环条件必须同时判 fast 和 fast.next 非空,fast 一次跨两步,少判一个就会空指针崩溃;二是把相遇点当环入口直接返回,跳过了整个阶段二;三是阶段二的两个指针必须严格同速各走一步,上面的数学推导建立在同速之上,速度不一致就永远不会在入口相遇。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3先 1 步/2 步追出相遇点,再「头 + 相遇点同速走」逼出入口。两阶段,记牢。
- 4阶段① 找相遇点。slow、fast 都站在头节点(值 6)。接下来 slow 每帧走 1 步、fast 走 2 步。
- 5slow 向后挪 1 格,来到值 3。fast 先按兵不动。
- 6fast 连走 2 格,来到值 7。它在环里追赶 slow。
- 7slow 向后挪 1 格,来到值 7。fast 先按兵不动。
- 8fast 连走 2 格,来到值 0。它在环里追赶 slow。
- 9slow 向后挪 1 格,来到值 2。fast 先按兵不动。
- 10fast 连走 2 格,来到值 9。它在环里追赶 slow。
- 11slow 向后挪 1 格,来到值 0。fast 先按兵不动。
- 12fast 连走 2 格,来到值 1。它在环里追赶 slow。
- 13slow 向后挪 1 格,来到值 8。fast 先按兵不动。
- 14fast 连走 2 格,来到值 2。它在环里追赶 slow。
- 15slow 向后挪 1 格,来到值 9。fast 先按兵不动。
- 16fast 连走 2 格,来到值 8。它在环里追赶 slow。
- 17slow 向后挪 1 格,来到值 5。fast 先按兵不动。
- 18fast 连走 2 格到值 5,正好和 slow 撞在同一节点——相遇点找到了(值 5)。
- 19相遇点(值 5)被点亮。注意它不一定是入口!接下来阶段② 才能把真正的入口逼出来。
- 20阶段② 找入口。p 回到头节点(值 6),q 留在相遇点(值 5)。两者从现在起每帧各走 1 步,同速。
- 21p、q 各走 1 步:p 到值 3,q 到值 1。还没相遇,继续。
- 22p、q 各走 1 步:p 到值 7,q 到值 4。还没相遇,继续。
- 23p 到值 2、q 也到值 2,二者撞在同一节点——这就是环入口,返回它。
- 24环入口 = 值 2 的节点(下标 3)。这正是尾巴 next 回指的那个节点,符合题意。
⚠️ 容易写错的地方
✗ 错:把相遇点当环入口直接返回
✓ 对:相遇点≠入口,必须做阶段②
相遇点是 fast 套圈追上 slow 的位置,通常在入口之后
✗ 错:阶段② 两指针不同速
✓ 对:p 和 slow 都每次只走 1 步
数学结论建立在「同速」上,快慢不一致就不会在入口相遇
✗ 错:循环条件只判 fast
✓ 对:判 fast 且 fast.next 都非空
fast 要走 2 步,next 为空再 .next 会空指针崩溃
完整代码(Python / C++ / Java)
Python
def detectCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next # 慢走 1 步
fast = fast.next.next # 快走 2 步
if slow is fast: # 阶段① 相遇
p = head
while p is not slow: # 阶段② 头与相遇点同速走
p = p.next
slow = slow.next
return p # 再相遇 = 环入口
return None # fast 到 null = 无环C++
ListNode* detectCycle(ListNode* head){
ListNode *slow = head, *fast = head;
while(fast && fast->next){
slow = slow->next; // 慢 1 步
fast = fast->next->next; // 快 2 步
if(slow == fast){ // 阶段① 相遇
ListNode* p = head;
while(p != slow){ // 阶段② 同速走
p = p->next;
slow = slow->next;
}
return p; // 环入口
}
}
return nullptr; // 无环
}Java
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 慢 1 步
fast = fast.next.next; // 快 2 步
if (slow == fast) { // 阶段① 相遇
ListNode p = head;
while (p != slow) { // 阶段② 同速走
p = p.next;
slow = slow.next;
}
return p; // 环入口
}
}
return null; // 无环
}复杂度
时间
O(n)
阶段① 最多走 n 步相遇,阶段② 走头到入口的距离 a,合起来线性
空间
O(1)
只用 slow/fast/p 三个指针,不开哈希表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 环形链表 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
只判断有没有环(LC141)和求入口(LC142)差在哪?+
LC141 到阶段① 相遇就能返回 true;LC142 还要多做阶段②——p 回头、与相遇点同速走,再相遇才是入口。
能用哈希表做吗?+
能:边走边把访问过的节点存进集合,第一个重复出现的节点就是入口。但要 O(n) 额外空间,面试更想看 O(1) 的快慢指针。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 环形链表 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。