算法困惑问答 · LC 141 环形链表
快慢指针为什么一定能判断链表有环?
直接答案
分两种情况看:链表无环,快指针一路向前,最多 n/2 步就撞到 null,结束;链表有环,两个指针迟早都进环,进环后快指针每轮比慢指针多走 1 步——相对距离每轮减 1,既不会跨过也不会拉开,最多绕一圈就追上慢指针。所以「相遇 ⇒ 有环、撞 null ⇒ 无环」两个出口覆盖了所有情况,算法必然终止且结论可靠。
把「追及」换算成相对速度
把慢指针当参照物:快指针每轮净逼近 1 格。设进环时两者相距 d(0 ≤ d < 环长),d 每轮严格减 1,走 d 轮后变成 0——正好落在同一个节点上,不存在「擦肩而过」。这就是操场跑圈的直觉:快的迟早从背后套圈追上慢的,而且因为每轮只追近一格,相遇瞬间一定是踩在同一格上。
这也回答了「为什么快指针走 2 步而不是 3 步」:步差为 1 是保证「不跨过」的关键。走 3 步时相对速度是 2,d 为奇数时会直接跳过慢指针,在环里反复错过,虽然数学上仍会相遇,但分析复杂且毫无收益。2 步是又对又好证的选择。
对比哈希表方案:赢在空间
用哈希表记录访问过的节点、遇到重复即有环,思路更直白,时间同样 O(n),但要 O(n) 额外空间。快慢指针只用两个指针变量,O(1) 空间拿下同样的结论——这正是面试官期待你写出它的原因。
工程细节一条:循环条件必须同时判 fast 和 fast.next 非空。fast 每次走两步,只判 fast 的话,fast.next 为 null 时再取 .next.next 直接空指针崩溃。
代码关键行(Python)
def hasCycle(head):
slow = fast = head # 同起点出发
while fast and fast.next: # 两个都要判空
slow = slow.next # 慢走 1 步
fast = fast.next.next # 快走 2 步
if slow is fast:
return True # 套圈相遇 = 有环
return False # 撞到 null = 无环`while fast and fast.next` 是最容易漏的细节:快指针一步跨两格,两个落脚点都得确认存在,否则无环链表在结尾处直接崩溃。
常见追问
相遇点就是环的入口吗?
不是。相遇点只证明有环。要找环入口(LC142),相遇后把一个指针放回 head,两个指针都改为每次走 1 步,再次相遇处就是入口——可以证明头到入口的距离恰好等于相遇点沿环走到入口的距离。
两个指针必须从同一起点出发吗?
建议同从 head 出发,相遇分析最干净。刻意错开起点也能判环,但「起点差」会混进距离推导里,LC142 找入口的结论就不再直接成立。
把这道题彻底吃透
LC 141「环形链表」有逐步交互动画和完整图文题解,配着本页的结论看,一遍就通。