环形链表 图解题解
链表里是否藏着一个圈?快慢指针一追一赶,答案自然浮现。
判断链表有没有环,就像操场上快慢两个跑步者:慢的一步、快的两步,若是直道(无环)快的早早冲出终点遇到 null;若跑道是个圈,快的迟早从后面套圈追上慢的——两人相遇就证明有环。整个过程只需两个指针变量,不用额外空间记录谁走过哪里。
这道题到底在问什么
- 输入
- head=[3,2,0,-4,7,8,1,9], 尾→下标2
- 输出
- true(有环)
最优解:为什么这么做
一句话答案: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 只差一步数学。
▶ 动画逐步走查(共 18 步)——想跟着动画一帧帧对照就展开
- 3记住这条:快每次 2 步、慢每次 1 步。相遇 = 有环;快先到 null = 无环。
- 4slow 和 fast 都站在头节点(值 3)。接下来 slow 每帧走 1 步、fast 走 2 步。
- 5slow 向后挪 1 格,来到值 2。fast 先按兵不动。
- 6fast 连走 2 格,来到值 0。它比 slow 快,正在环里追赶。
- 7slow 向后挪 1 格,来到值 0。fast 先按兵不动。
- 8fast 连走 2 格,来到值 7。它比 slow 快,正在环里追赶。
- 9slow 向后挪 1 格,来到值 -4。fast 先按兵不动。
- 10fast 连走 2 格,来到值 1。它比 slow 快,正在环里追赶。
- 11slow 向后挪 1 格,来到值 7。fast 先按兵不动。
- 12fast 连走 2 格,来到值 0。它比 slow 快,正在环里追赶。
- 13slow 向后挪 1 格,来到值 8。fast 先按兵不动。
- 14fast 连走 2 格,来到值 7。它比 slow 快,正在环里追赶。
- 15slow 向后挪 1 格,来到值 1。fast 先按兵不动。
- 16fast 连走 2 格到值 1,正好和 slow 撞在同一节点——两指针相遇,说明链表有环。
- 17相遇点(值 1)被点亮。快指针在环里一圈圈套,必然追上慢指针,结论:有环,返回 true。
- 18换一条没有环的短链。同样 slow 走 1 步、fast 走 2 步,看 fast 怎样先撞 null。
- 19slow 到值 2,fast 到值 3。fast 已经领先一截。
- 20fast 再走 2 步直接越过尾节点 4 抵达 null——没有环把它兜回来。判定无环,返回 false。
⚠️ 容易写错的地方
✗ 错:循环条件只判 fast
✓ 对:要判 fast 且 fast.next 都非空
fast 每次走 2 步,next 为空再 .next 会空指针崩溃
✗ 错:slow、fast 起点错开
✓ 对:两者都从 head 出发
同起点才能保证相遇分析成立
✗ 错:用哈希记录节点
✓ 对:快慢指针 O(1) 空间
哈希能做但多花 O(n) 空间,面试更想看双指针
完整代码(Python / C++ / Java)
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 # fast 到 null = 无环C++
bool hasCycle(ListNode* head){
ListNode *slow = head, *fast = head;
while(fast && fast->next){
slow = slow->next; // 慢 1 步
fast = fast->next->next; // 快 2 步
if(slow == fast) return true;
}
return false;
}Java
public boolean hasCycle(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) return true;
}
return false;
}复杂度
时间
O(n)
有环时慢指针进环后,快指针每轮逼近 1 格,最多走环长就追上
空间
O(1)
只用两个指针,不开额外哈希表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 环形链表 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
相遇后怎么求环的入口节点(LC142)?+
相遇后让一个指针回到 head,两指针都每次走 1 步,再次相遇处就是环入口(数学可证头到入口的距离 = 相遇点绕到入口的距离)。
快指针为什么走 2 步而不是 3 步?+
2 步保证相对速度差恰为 1,每轮稳定逼近 1 格、不会跨过;步长更大可能在环里反复错过。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 环形链表 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。