题目描述
思路解析
一句话答案: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 一次跨两步,少判一个就会空指针崩溃;二是把相遇点当环入口直接返回,跳过了整个阶段二;三是阶段二的两个指针必须严格同速各走一步,上面的数学推导建立在同速之上,速度不一致就永远不会在入口相遇。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
先 1 步/2 步追出相遇点,再「头 + 相遇点同速走」逼出入口。两阶段,记牢。
阶段① 找相遇点。slow、fast 都站在头节点(值 6)。接下来 slow 每帧走 1 步、fast 走 2 步。
slow 向后挪 1 格,来到值 3。fast 先按兵不动。
fast 连走 2 格,来到值 7。它在环里追赶 slow。
slow 向后挪 1 格,来到值 7。fast 先按兵不动。
fast 连走 2 格,来到值 0。它在环里追赶 slow。
slow 向后挪 1 格,来到值 2。fast 先按兵不动。
fast 连走 2 格,来到值 9。它在环里追赶 slow。
slow 向后挪 1 格,来到值 0。fast 先按兵不动。
fast 连走 2 格,来到值 1。它在环里追赶 slow。
slow 向后挪 1 格,来到值 8。fast 先按兵不动。
fast 连走 2 格,来到值 2。它在环里追赶 slow。
slow 向后挪 1 格,来到值 9。fast 先按兵不动。
fast 连走 2 格,来到值 8。它在环里追赶 slow。
slow 向后挪 1 格,来到值 5。fast 先按兵不动。
fast 连走 2 格到值 5,正好和 slow 撞在同一节点——相遇点找到了(值 5)。
相遇点(值 5)被点亮。注意它不一定是入口!接下来阶段② 才能把真正的入口逼出来。
阶段② 找入口。p 回到头节点(值 6),q 留在相遇点(值 5)。两者从现在起每帧各走 1 步,同速。
p、q 各走 1 步:p 到值 3,q 到值 1。还没相遇,继续。
p、q 各走 1 步:p 到值 7,q 到值 4。还没相遇,继续。
p 到值 2、q 也到值 2,二者撞在同一节点——这就是环入口,返回它。
环入口 = 值 2 的节点(下标 3)。这正是尾巴 next 回指的那个节点,符合题意。
空链表、无环、单节点自环这三种先想清楚,别让阶段② 死循环。
面试常把 141、142 连着问,要能讲清「相遇」之后多出来的阶段②。
参考代码
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 = 无环复杂度
- 时间:O(n),阶段① 最多走 n 步相遇,阶段② 走头到入口的距离 a,合起来线性
- 空间:O(1),只用 slow/fast/p 三个指针,不开哈希表
易错点
面试追问把动画讲成自己的话
追问只判断有没有环(LC141)和求入口(LC142)差在哪?
追问能用哈希表做吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
两两交换链表中的节点
LeetCode 24 · 中等 · 沿着 链表 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题