环形链表刷题笔记(LeetCode热题100--141、142)
一、核心概念
1.1 环形链表的定义
- 环形链表:链表中存在一个节点,可以通过连续追踪
next指针再次到达 - 环入口:进入环的第一个节点
- 环长度:环中节点的个数
1.2 快慢指针原理
- 快指针:每次走两步
- 慢指针:每次走一步
- 核心思想:如果有环,快指针最终会追上慢指针(相遇)
二、两道经典题目对比
2.1 141. 环形链表(简单)
目标:只判断链表是否有环
方法一:哈希表法
cpp
bool hasCycle(ListNode *head) {
unordered_set<ListNode*> set;
while(head != NULL) {
if(set.count(head)) return true; // 节点已存在,说明有环
set.insert(head);
head = head->next;
}
return false; // 遍历到链表尾部,无环
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
- 优点:思路简单直接
- 缺点:需要额外空间
方法二:快慢指针法(推荐)
cpp
bool hasCycle(ListNode *head) {
if(head == nullptr || head->next == nullptr) return false;
ListNode *f = head->next; // 快指针(先走一步)
ListNode *s = head; // 慢指针
while(s != f) {
if(f == nullptr || f->next == nullptr) return false;
s = s->next;
f = f->next->next;
}
return true; // 快慢指针相遇,说明有环
}
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 注意:快指针从
head->next开始,避免第一次就相等
2.2 142. 环形链表 II(中等)
目标:找出环形链表的环入口节点
Floyd算法实现
cpp
ListNode *detectCycle(ListNode *head) {
if(head == nullptr) return nullptr;
ListNode *s = head; // 慢指针
ListNode *f = head; // 快指针
// 第一阶段:检测是否有环
while(true) {
if(f == nullptr || f->next == nullptr) return nullptr; // 无环
s = s->next;
f = f->next->next;
if(f == s) break; // 有环,相遇
}
// 第二阶段:找到环入口
f = head; // 快指针重新指向头部
while(s != f) {
s = s->next;
f = f->next;
}
return f; // 相遇点就是环入口
}
三、算法推导与证明
3.1 数学推导
slow指针每次走1步、fast指针每次走2步
如果fast 指针走过链表末端,说明链表无环,此时直接返回 null。
否则存在环,并且f快,s慢,当它们进入环后,相当于追及问题,二者一定会相遇:

我们设:
- 从 head 到环入口的距离为 a(你图中 a=3)
- 环的长度为 b(你图中 b=4)
- 第一次相遇时,slow 指针走了
s步,因为 slow 和 fast 指针走的时间相同,但 fast 指针的速度是 slow 的2倍。所以,fast 指针走了f = 2s步。 - fast 比 slow 多走了 n 个环的长度,即
f=s+nb;( 解析: 双指针都走过 a 步,然后在环内绕圈直到重合,重合时 fast 比 slow 多走 环的长度整数倍 )。
将以上两式相减得到f=2nb,s=nb,即 fast 和 slow 指针分别走了 2n,n 个环的周长。 - 如果让指针从链表头部一直向前走并统计步数k,那么所有 走到链表入口节点时的步数 是:
k=a+nb,即先走 a 步到入口节点,之后每绕 1 圈环( b 步)都会再次到入口节点。 - 而目前 slow 指针走了 nb 步。因此, slow 再走 a 步即可到达环的入口。
- a是未知数,我们让fast指针指向head结点,再依次遍历链表,直到走了a步到达环的入口,此时slow也走了a步,二者相遇,得到结果。
3.2 图示理解
text
slow(s) 指针每次走 1 步
fast(f) 指针每次走 2 步(f = 2s)
head 是链表起点
a = 3:从 head 到环入口的距离(节点1→2→3→4,共3步)
b = 4:环的长度(节点4→5→6→7→8→4,共4个节点)
第一次相遇 在节点6(环内某点)
第二次相遇 在节点4(环入口)------此时将 slow 从 head 重新出发,fast 从第一次相遇点出发,
都每次走1步,相遇点即为环入口
四、解题要点总结
4.1 边界条件处理
- 空链表 :直接返回
false或nullptr - 单节点 :检查
head->next是否为空 - 快指针判空 :必须检查
fast和fast->next是否为空
4.2 代码实现技巧
- 141题的简洁写法
cpp
bool hasCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while(fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if(slow == fast) return true;
}
return false;
}
- 142题的关键点
cpp
f = head; // 重置快指针到头部
while(s != f) { // 同速前进
s = s->next;
f = f->next;
}
4.3 常见错误
- 忘记检查
fast->next是否为空 - 141题中快慢指针起始位置相同导致误判
- 142题中忘记在找到环后重置指针
五、面试常见问题
5.1 时间复杂度分析
- 141题:O(n),最坏情况下遍历整个链表
- 142题:O(n),两阶段都不会超过链表长度
5.2 空间复杂度分析
- 哈希表法:O(n)
- 快慢指针法:O(1) ← 推荐
5.3 扩展问题
-
如何求环的长度?
- 相遇后,固定一个指针,另一个继续走直到再次相遇
-
如果快指针走3步、慢指针走1步,还能检测环吗?
- 不一定,可能会错过
六、刷题建议
- 先理解原理:掌握Floyd算法的数学推导
- 熟练两种方法:哈希表法和快慢指针法
- 注意边界条件:空链表、单节点链表
- 画图分析:在纸上画出指针移动过程
- 代码模板:熟记142题的代码结构,可以应对大部分变体
总结:环形链表问题考察对指针操作和数学推导的理解。141题是基础,142题是进阶,掌握快慢指针法是解决这类问题的核心技能。