📌 题目链接:141. 环形链表 - 力扣(LeetCode)
🔍 难度:简单 | 🏷️ 标签:链表、双指针、哈希表、Floyd判圈
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(最优解)
💡 面试重点:本题是链表中“环检测”的经典问题,常用于考察对「快慢指针」的理解与应用能力。在实际系统中,如内存泄漏检测、循环依赖判断等场景都有其影子。掌握此题能帮助你应对后续更复杂的链表结构问题,例如 142. 环形链表 II。
🧠 一、题目分析
给定一个链表的头节点 head,判断链表中是否存在环。
📌 关键点:
- 若存在某个节点可以通过
next指针再次到达,则说明有环。 pos是评测系统内部使用的参数,表示尾部连接到链表中的位置(索引从 0 开始),但不作为输入传递。- 返回
true表示有环,否则返回false。
💡 示例解析:
示例1: [3,2,0,-4], pos = 1
↓ ↑
──────────┘
3 → 2 → 0 → -4 → 2 (环)
即最后一个节点 -4 的 next 指向了第 1 个节点 2,形成环。
⚠️ 注意:题目不要求我们找到环的入口,只需判断是否存在即可。
🔍 二、核心算法及代码讲解
✅ 方法一:哈希表(Hash Set)
📌 思路
遍历链表时,将每个访问过的节点存入哈希表。如果某次访问到了已存在的节点,说明出现了重复访问,即存在环。
💡 优点
- 思路直观清晰,容易理解和实现。
- 时间复杂度低,适合初学者。
❌ 缺点
- 使用额外空间存储节点地址,空间复杂度为 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:
bool hasCycle(ListNode* head) {
unordered_set<ListNode*> seen; // 哈希表记录已访问的节点
while (head != nullptr) { // 遍历链表直到结尾
if (seen.count(head)) { // 如果当前节点已在集合中,说明有环
return true;
}
seen.insert(head); // 将当前节点加入集合
head = head->next; // 移动到下一个节点
}
return false; // 遍历完无重复,无环
}
};
📊 复杂度分析
- 时间复杂度:O(N),最坏情况下需遍历所有节点一次。
- 空间复杂度:O(N),哈希表最多存储 N 个节点。
✅ 方法二:快慢指针(Floyd 判圈算法)🔥 推荐!
📌 背景知识:Floyd 判圈算法(龟兔赛跑)
🐢 名称来源:“乌龟”代表慢指针,“兔子”代表快指针。
这是由 Robert W. Floyd 提出的经典算法,用于检测单向链表中是否存在环,并可以进一步定位环的入口。
🌀 原理详解
假设链表分为两部分:
- 非环部分长度:
a - 环的长度:
b
设慢指针速度为 1,快指针速度为 2。
当它们同时出发时:
- 慢指针走的距离:
d_slow = a + k*b + r(k 是绕环次数) - 快指针走的距离:
d_fast = 2*(a + k*b + r)
由于快指针比慢指针多走一圈或多圈,最终会追上慢指针(相遇)。
✅ 关键结论:只要链表中有环,快慢指针一定会相遇;否则快指针先走到
nullptr。
🔧 实现细节
| 细节 | 解释 | ||
|---|---|---|---|
slow = head, fast = head->next | 避免初始相等导致 while 不执行 | ||
while(slow != fast) | 循环条件:未相遇则继续 | ||
| `if(fast == nullptr | fast->next == nullptr)` | 防止空指针访问 |
🧩 代码实现
#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:
bool hasCycle(ListNode* head) {
// 边界情况:空链表或只有一个节点
if (head == nullptr || head->next == nullptr) {
return false;
}
ListNode* slow = head; // 慢指针,每次走一步
ListNode* fast = head->next; // 快指针,每次走两步
while (slow != fast) { // 只要没有相遇就继续
if (fast == nullptr || fast->next == nullptr) {
return false; // 快指针走到尽头,无环
}
slow = slow->next; // 慢指针前进一步
fast = fast->next->next; // 快指针前进两步
}
return true; // 相遇说明有环
}
};
📊 复杂度分析
- 时间复杂度:O(N)
- 无环时:快指针最多走 2N 步,仍为 O(N)
- 有环时:相遇最多发生在 N 步内(环长 ≤ N)
- 空间复杂度:O(1),仅使用两个指针变量。
✅ 面试加分项:能够说出这是 Floyd 判圈算法,并解释其原理,是非常加分的表现!
🧭 三、解题思路(分步拆解)
-
判断边界情况
- 如果
head为空 或head->next为空 → 无法成环 → 返回false
- 如果
-
初始化双指针
slow = headfast = head->next
-
进入循环
- 当
slow != fast时持续移动 - 每轮:
- 检查
fast是否越界(防止空指针访问) slow向前一步fast向前两步
- 检查
- 当
-
判断结果
- 若
slow == fast→ 说明相遇 → 有环 → 返回true - 若
fast先走到nullptr→ 无环 → 返回false
- 若
-
注意细节
- 初始位置设置避免死循环
- 快指针必须检查
fast->next是否为空
🧪 四、代码测试与验证
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 构造测试用例1: [3,2,0,-4], pos=1(-4指向2)
ListNode* node1 = new ListNode(3);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(0);
ListNode* node4 = new ListNode(-4);
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = node2; // 形成环,指向node2
Solution sol;
cout << "Test Case 1 (has cycle): " << (sol.hasCycle(node1) ? "true" : "false") << endl;
// 构造测试用例2: [1,2], pos=0(2指向1)
ListNode* node5 = new ListNode(1);
ListNode* node6 = new ListNode(2);
node5->next = node6;
node6->next = node5;
cout << "Test Case 2 (has cycle): " << (sol.hasCycle(node5) ? "true" : "false") << endl;
// 构造测试用例3: [1], pos=-1(无环)
ListNode* node7 = new ListNode(1);
cout << "Test Case 3 (no cycle): " << (sol.hasCycle(node7) ? "true" : "false") << endl;
return 0;
}
🔧 输出预期:
Test Case 1 (has cycle): true
Test Case 2 (has cycle): true
Test Case 3 (no cycle): false
🎯 五、面试拓展 & 常见提问
❓ Q1:为什么快慢指针一定能相遇?
因为在环中,快指针每次比慢指针多走一步,相当于在缩小两者之间的距离。只要环存在,距离最终会归零,必然相遇。
❓ Q2:能否用 do-while 替代 while?
可以!但要注意初始值相同的情况。若改为:
do {
...
} while (slow != fast);
则需确保初始时 slow != fast,否则跳过循环体。
❓ Q3:如何找到环的入口?(延伸至 142 题)
这是下一题的核心内容。当快慢指针相遇后,再让其中一个指针回到起点,然后两个指针同速前进,再次相遇的位置就是环的入口。
🔗 下一期预告:142. 环形链表 II —— 找环入口!
✅ 六、总结对比表
| 方法 | 时间复杂度 | 空间复杂度 | 是否推荐 | 适用场景 |
|---|---|---|---|---|
| 哈希表 | O(N) | O(N) | ⚠️ 初学可用 | 易理解,易调试 |
| 快慢指针 | O(N) | O(1) | ✅ 强烈推荐 | 面试必备,效率高 |
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第26题——142.环形链表 II(中等)
🔹 题目:给定一个链表,返回环的起始节点。如果无环,返回
nullptr。🔹 核心思路:在快慢指针相遇后,将其中一个指针移回头节点,两个指针以相同速度前进,再次相遇的位置即为环的入口。
🔹 考点:双指针、数学推导、链表环检测进阶。
🔹 难度:中等,是很多图论和系统设计题的基础。
💡 提示:记住公式
a = c - b,其中a是头到入口距离,c是相遇点到入口距离,b是环长。
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!
快慢指针来追击,
一快一慢不停息。
若有环儿必相遇,
无环终点早结束。
🚀 加油,算法之路,我们一起走到底!