最近在刷 LeetCode 的时候,遇到了一道经典题目:给定一个链表的头节点,返回链表开始入环的第一个节点;如果无环,就返回 null 。题目还特别强调:不能修改链表 。看题解了解到了 Floyd 判圈算法(快慢指针) ,感觉本质上还是数学
一开始我对这个算法只是有个模糊的印象------"快慢指针能判断有没有环",但具体怎么找到环的入口?为什么那样做是对的?这些细节我其实并不清楚。于是,我决定花点时间彻底搞懂它,并把整个思考过程记录下来。
🧩 问题理解
首先明确题意:
- 链表可能有环,也可能没有。
- 如果有环,我们要返回环的起始节点(即第一个被重复访问的节点)。
- 不能修改链表结构(比如打标记、反转等都不行)。
所以,我们的目标是:只通过指针移动,在 O(1) 空间内找到环的入口。
🔍 第一步:如何判断链表有环?
这一步我之前学过------用快慢指针(Floyd 判圈算法的第一阶段):
slow每次走 1 步,fast每次走 2 步。- 如果链表无环,
fast最终会走到null。 - 如果有环,
fast一定会在某个时刻追上slow(因为它们在环里跑,速度差为 1)。
这部分逻辑不难,代码也简单:
python
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# 有环!
break
else:
return None # 无环
但问题来了:相遇点 ≠ 环的入口。那怎么从相遇点找到真正的入口呢?
🤔 第二步:为什么重置一个指针到 head 就能找到入口?
这是我最困惑的地方。网上很多教程直接说:"把 slow 放回 head,然后两个指针一起走,相遇就是入口。"
但为什么?这背后一定有数学依据。
于是我画了个图,设了几个变量:
a:从链表头到环入口的距离。b:从环入口到快慢指针第一次相遇点的距离。c:从相遇点再回到环入口的距离(所以环总长 =b + c)。
当 slow 和 fast 第一次相遇时:
slow走了a + b步。fast走了a + b + n(b + c)步(n ≥ 1,因为它可能绕了多圈)。
又因为 fast 的速度是 slow 的两倍,所以路程也是两倍:
2(a + b) = a + b + n(b + c)
=> a + b = n(b + c)
=> a = (n - 1)(b + c) + c
这个式子太关键了!
它说明:从 head 出发走 a 步,等于从相遇点出发走 c 步,再加上若干整圈。
而 c 正好是从相遇点回到环入口的距离!
所以,如果我们:
- 把
slow重置到head; - 让
slow和fast都以每次 1 步的速度前进;
那么当 slow 走了 a 步到达入口时,fast 也刚好从相遇点走了 a 步------而根据上面的等式,这正好让它也到达入口!
于是,它们会在环的入口处相遇。
💡 这一刻我恍然大悟:原来不是"碰巧",而是数学必然!
✅ 完整实现
结合以上分析,代码就清晰了:
python
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
def detectCycle(head: ListNode) -> ListNode:
if not head or not head.next:
return None
slow = fast = head
# 第一阶段:检测是否有环
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None # 无环
# 第二阶段:找环的入口
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow # 或 fast,此时两者相等
⚠️ 实现中的注意事项
在写代码时,有几个容易踩的坑,我总结如下:
-
边界情况要处理
- 空链表(
head is None)或只有一个节点且无环的情况,直接返回None。
- 空链表(
-
循环条件别写错
- 必须是
while fast and fast.next,否则fast.next.next会报错。
- 必须是
-
不要提前重置指针
- 只有在确认有环(即
slow == fast)之后,才能重置slow = head。
- 只有在确认有环(即
-
空间复杂度是 O(1)
- 整个过程只用了两个指针,完全符合题目"不允许修改链表"和"常数空间"的要求。
-
时间复杂度是 O(n)
- 第一阶段最多走 2n 步,第二阶段最多走 n 步,总体线性。
🧪 举个例子验证一下
假设链表是:3 → 2 → 0 → -4 → 2(环),即 pos = 1。
a = 1(head 到节点 2)- 环是
2 → 0 → -4 → 2,长度为 3 - 快慢指针可能在
-4相遇(b = 2,c = 1)
根据公式:a = (n-1)*3 + 1,当 n=1 时,a = 1,成立。
重置 slow 到 head(3),然后:
slow: 3 → 2fast: -4 → 2
在节点 2 相遇,正是环的入口!✅