题目描述
思路解析
一句话答案:LeetCode 141 环形链表的最优解是快慢指针(Floyd 判圈):slow 每次走 1 步、fast 每次走 2 步,链表有环则 fast 必在环内追上 slow,两指针相遇即返回 true;无环则 fast 先走到 null 返回 false。时间 O(n)、空间 O(1),不需要哈希表记录走过的节点。
这道题真正在问什么
给一条单链表,判断其中是否存在环——即某个节点的 next 指回了前面出现过的节点。有环的链表沿 next 一直走永远到不了 null,朴素的「走到头就没环」会陷进死循环,所以问题的实质是:如何在有限步内区分「还没走到头」和「永远走不到头」。
为什么记录访问过的节点还不够好
最直觉的解法是用哈希表存下每个走过的节点:再次遇到已存在的节点说明兜了圈,有环;走到 null 说明无环。时间 O(n),思路也干净,但要 O(n) 的额外空间。题目的进阶要求是 O(1) 空间——链表可能极长,这个内存开销值得省,也正是这个约束把快慢指针逼了出来。
为什么快慢指针有环时一定会相遇
让 slow 每次走 1 步、fast 每次走 2 步,同时从头出发。无环时 fast 在前面开路,先撞到 null,循环结束返回 false,不存在漏判。
有环时,fast 先进环,slow 随后也进环。从此两者都在环上转圈,用相对运动看最清楚:fast 相对 slow 每一轮净逼近 1 步。两者的环上距离是一个严格递减的非负整数,每轮减一、不可能被跳过,减到 0 的那一刻两指针落在同一个节点上——这就是「相遇」。所以有环必相遇、无环必到 null,两种结局覆盖所有情况,算法一定终止。
这也回答了「为什么 fast 走 2 步而不是 3 步」:步差为 1 保证距离逐格递减、不会隔着跨过去;步差更大时逼近过程可能反复错过,正确性论证会复杂得多。2 步是让证明最简单的选择。
循环条件为什么要同时判 fast 和 fast.next
fast 一次要跨两步,即先取 fast.next 再取它的 next。若只检查 fast 非空,当 fast 停在尾节点时 fast.next 是 null,再取一层 next 就是空指针崩溃。所以循环条件必须是 fast 和 fast.next 都非空,这同时天然处理了空链表和单节点无环链表——循环体一次都不进,直接返回 false。
另一个细节是相遇判断用节点身份(是不是同一个对象)而不是节点值,链表里完全可能有多个值相同的不同节点。
复杂度怎么数,相关的进阶题
时间 O(n):slow 进环之前最多走链长步;进环后 fast 与 slow 的距离不超过环长,每轮缩 1,最多再走环长步就相遇,总步数是线性的。空间 O(1):自始至终只有两个指针。
这套 Floyd 判圈还有一个漂亮的续集:LeetCode 142 要求找出环的入口。做法是相遇后把一个指针放回头节点,两个指针改为同速各走 1 步,再次相遇的位置就是环入口——背后是「头到入口的距离等于相遇点沿环走到入口的距离」这条等式。判环是那道题的前置零件,本题吃透了,142 只差一步数学。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条:快每次 2 步、慢每次 1 步。相遇 = 有环;快先到 null = 无环。
slow 和 fast 都站在头节点(值 3)。接下来 slow 每帧走 1 步、fast 走 2 步。
slow 向后挪 1 格,来到值 2。fast 先按兵不动。
fast 连走 2 格,来到值 0。它比 slow 快,正在环里追赶。
slow 向后挪 1 格,来到值 0。fast 先按兵不动。
fast 连走 2 格,来到值 7。它比 slow 快,正在环里追赶。
slow 向后挪 1 格,来到值 -4。fast 先按兵不动。
fast 连走 2 格,来到值 1。它比 slow 快,正在环里追赶。
slow 向后挪 1 格,来到值 7。fast 先按兵不动。
fast 连走 2 格,来到值 0。它比 slow 快,正在环里追赶。
slow 向后挪 1 格,来到值 8。fast 先按兵不动。
fast 连走 2 格,来到值 7。它比 slow 快,正在环里追赶。
slow 向后挪 1 格,来到值 1。fast 先按兵不动。
fast 连走 2 格到值 1,正好和 slow 撞在同一节点——两指针相遇,说明链表有环。
相遇点(值 1)被点亮。快指针在环里一圈圈套,必然追上慢指针,结论:有环,返回 true。
换一条没有环的短链。同样 slow 走 1 步、fast 走 2 步,看 fast 怎样先撞 null。
slow 到值 2,fast 到值 3。fast 已经领先一截。
fast 再走 2 步直接越过尾节点 4 抵达 null——没有环把它兜回来。判定无环,返回 false。
空链表和单节点先想清,别让循环条件漏掉它们。
判环之后最高频的追问就是「求入口」。
参考代码
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 # fast 到 null = 无环复杂度
- 时间:O(n),有环时慢指针进环后,快指针每轮逼近 1 格,最多走环长就追上
- 空间:O(1),只用两个指针,不开额外哈希表
易错点
面试追问把动画讲成自己的话
追问相遇后怎么求环的入口节点(LC142)?
追问快指针为什么走 2 步而不是 3 步?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
寻找重复数
LeetCode 287 · 中等 · 沿着 链表 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题