📌 题目链接:142. 环形链表 II - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:链表、双指针、哈希表、环检测
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(最优解) vs O(n)(哈希表解法)
🔍 题目分析
给定一个链表的头节点
head,返回链表开始入环的第一个节点。如果无环,则返回null。
这道题是 “环形链表 I” 的进阶版本,不仅要判断是否有环,还要定位环的入口点。
✅ 关键信息提炼:
- ❌ 不能修改链表结构。
- ✅ 节点值可能重复,因此不能通过值来判断是否重复访问。
- ✅ 必须找到第一个进入环的节点(即环的入口)。
- 🧠 核心难点:如何在不使用额外空间的情况下,精准定位环的起点?
🧠 核心算法及代码讲解
本题有两个经典解法:
| 方法 | 时间复杂度 | 空间复杂度 | 是否推荐面试 |
|---|---|---|---|
| 哈希表 | O(n) | O(n) | ✅ 简单易懂,但非最优 |
| 快慢指针(Floyd 判圈算法) | O(n) | O(1) | ✅✅✅ 面试必考,优雅高效 |
我们重点讲解 快慢指针法(Floyd Cycle Detection Algorithm),它是图论和链表中环检测的经典算法。
🌀 Floyd 判圈算法原理(又称龟兔赛跑)
🐢 兔子(fast)走两步,乌龟(slow)走一步
while (fast != nullptr) {
slow = slow->next;
if (fast->next == nullptr) return nullptr; // fast 后面没节点了,无环
fast = fast->next->next;
if (fast == slow) break; // 相遇!说明有环
}
🔁 为什么能相遇?
假设:
- 链表中环外部分长度为
a - 环内部分长度为
b + c(其中b是从入口到相遇点的距离,c是环的剩余部分) - 当
fast和slow第一次相遇时,fast已经走了2x步,slow走了x步
由于 fast 比 slow 快一倍,且都在环里绕圈,最终必然相遇。
📐 数学推导(关键!)
设:
a:从头节点到环入口的距离b:从环入口到相遇点的距离c:环的其余部分长度 → 环总长 =b + c
当 fast 和 slow 相遇时:
slow走了:a + bfast走了:a + b + n(b + c)(n 是圈数)
因为 fast 速度是 slow 的两倍:
2(a + b) = a + b + n(b + c)
=> a + b = n(b + c)
=> a = n(b + c) - b
=> a = (n - 1)(b + c) + c
💡 这个公式告诉我们:从头节点走到环入口的距离 = 从相遇点绕环若干圈再走 c 的距离
所以我们可以:
- 让
slow继续走 - 新建一个指针
ptr从头开始走 - 两者每次走一步,会在环入口处相遇
✅ 快慢指针法完整流程图示:
a b c
[头]----->[环入口]---->[相遇点]----->[环入口]
↑ ↑
ptr slow
↑
fast
当 ptr 和 slow 同时移动,它们会在环入口处相遇!
🧩 解题思路(分步详解)
- 初始化两个指针:
slow和fast都指向head - 移动指针:
slow每次走 1 步fast每次走 2 步
- 检查是否相遇:
- 如果
fast或fast->next为空 → 无环,返回nullptr - 如果
fast == slow→ 有环,进入下一步
- 如果
- 寻找环入口:
- 重置一个指针
ptr指向head ptr和slow同时每步走 1 步- 它们相遇的地方就是环的入口
- 重置一个指针
- 返回结果
📊 算法分析
| 指标 | 说明 |
|---|---|
| 时间复杂度 | O(n):最多遍历链表两次,第一次找相遇点,第二次找入口 |
| 空间复杂度 | O(1):只用了三个指针变量 |
| 适用场景 | 所有需要检测环并找入口的问题,如:约瑟夫问题、链表成环、循环依赖检测等 |
| 面试价值 | ⭐⭐⭐⭐⭐ 高频考察点,尤其在大厂系统设计或底层优化岗位中常见 |
🚨 注意:虽然哈希表方法简单,但空间复杂度高,不适合内存敏感场景;而快慢指针法体现了算法之美——用极小空间解决复杂问题。
💻 代码实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while (fast != nullptr) {
slow = slow->next;
if (fast->next == nullptr) {
return nullptr;
}
fast = fast->next->next;
if (fast == slow) {
ListNode *ptr = head;
while (ptr != slow) {
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return nullptr;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 构造测试用例 1: [3,2,0,-4], pos = 1
ListNode* head1 = new ListNode(3);
head1->next = new ListNode(2);
head1->next->next = new ListNode(0);
head1->next->next->next = new ListNode(-4);
head1->next->next->next->next = head1->next; // 形成环,pos=1
Solution sol;
ListNode* result1 = sol.detectCycle(head1);
if (result1) {
cout << "Test 1: Found cycle entry at node with value: " << result1->val << endl;
} else {
cout << "Test 1: No cycle found." << endl;
}
// 构造测试用例 2: [1,2], pos = 0
ListNode* head2 = new ListNode(1);
head2->next = new ListNode(2);
head2->next->next = head2; // 形成环,pos=0
ListNode* result2 = sol.detectCycle(head2);
if (result2) {
cout << "Test 2: Found cycle entry at node with value: " << result2->val << endl;
} else {
cout << "Test 2: No cycle found." << endl;
}
// 构造测试用例 3: [1], pos = -1 (无环)
ListNode* head3 = new ListNode(1);
ListNode* result3 = sol.detectCycle(head3);
if (result3) {
cout << "Test 3: Found cycle entry at node with value: " << result3->val << endl;
} else {
cout << "Test 3: No cycle found." << endl;
}
return 0;
}
🛠️ 补充知识:Floyd 算法的扩展应用
🧩 应用 1:求环的长度
一旦找到相遇点,可以让 fast 保持不动,slow 继续走,直到再次相遇,所走步数即为环长。
int getCycleLength(ListNode* meetPoint) {
ListNode* temp = meetPoint;
int length = 0;
do {
temp = temp->next;
length++;
} while (temp != meetPoint);
return length;
}
🧩 应用 2:用于检测循环依赖(如对象引用、任务调度)
在实际系统中,比如 Java GC 中的可达性分析、操作系统中的死锁检测,都会用到类似逻辑。
🧩 应用 3:约瑟夫问题变种
某些约瑟夫问题可以通过构建虚拟链表 + 快慢指针优化求解。
🧪 测试用例验证
| 输入 | 输出 | 说明 |
|---|---|---|
[3,2,0,-4], pos=1 | 返回索引为 1 的节点 | 环入口是 2 |
[1,2], pos=0 | 返回索引为 0 的节点 | 环入口是 1 |
[1], pos=-1 | null | 无环 |
[] | null | 空链表 |
✅ 所有边界情况都已覆盖:空链表、单节点、多节点、环入口在头部或中间。
🎯 面试常问问题 & 回答技巧
Q1:为什么快慢指针一定能相遇?
A:只要存在环,
fast比slow快,且在有限步内会追上。即使fast多绕几圈,也终将相遇。
Q2:为什么从头节点和相遇点同时出发,能在入口相遇?
A:由数学推导得出:
a = (n-1)(b+c) + c,意味着从头走a步和从相遇点绕圈后走c步到达同一位置。
Q3:能否用栈或递归解决?
A:可以,但空间复杂度至少 O(n),不如双指针优。面试官更看重空间优化能力。
Q4:有没有其他方法?
A:还有「三指针法」、「标记法」(改值),但都不推荐,破坏原始数据或增加复杂度。
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第27题 —— 21.合并两个有序链表(简单)
🔹 题目:给定两个已排序的链表,将它们合并为一个新的有序链表,返回合并后的头节点。
🔹 核心思路:使用双指针分别遍历两个链表,比较节点值,较小者加入新链表。
🔹 考点:链表操作、递归 vs 迭代、边界处理、原地合并技巧。
🔹 难度:简单,但却是链表基础中的“压轴题”,很多后续题目(如合并K个链表)都是其扩展。
🔹 面试频率:极高!几乎每轮技术面都会出现变体!
💡 提示:不要用
new创建新节点(除非题目允许),优先考虑原地合并或递归解法!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!
📌 附录:相关优质资源推荐
- 📘 《算法导论》第 22 章:图的表示与遍历(含判圈算法)
- 🔗 LeetCode 官方题解:leetcode.com/problems/de…
- 🎥 B站视频推荐:Floyd判圈算法动画演示(搜索关键词:“龟兔赛跑 环检测”)
✨ 坚持每日一题