一、题目简介
给你一个链表的头节点 head,判断链表中是否有环。 如果链表中某个节点,可以通过连续跟踪 next 指针再次到达,则说明链表中存在环。
- 存在环 → 返回
true - 不存在环 → 返回
false
注意:题目中的
pos是评测系统内部用来标识环的位置,不会作为参数传入,我们只需要根据 head 判断即可。
二、直观解法:HashSet
1. 思路
这应该是所有人第一反应都能想到的解法: 遍历链表,每访问一个节点,就把它存到集合里。
- 如果当前节点已经在集合中:说明绕回来了,存在环
- 如果走到
null都没重复:说明没有环
2. 非常重要的细节
❌ 错误做法:存节点的 val 值 ✅ 正确做法:存节点对象本身
原因:链表中不同节点完全可能有相同的值。 比如 1 → 2 → 1 → 3 → null,值 1 出现了两次,但这是一条正常的无环链表。 只有同一个节点对象被第二次访问,才能说明存在环。
3. 完整代码
java
import java.util.HashSet;
import java.util.Set;
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
while (head != null) {
// 当前节点已经访问过 → 有环
if (set.contains(head)) {
return true;
}
// 没访问过,加入集合
set.add(head);
head = head.next;
}
// 走到null都没重复 → 无环
return false;
}
}
4. 复杂度分析
-
时间复杂度:O (n) 每个节点最多遍历一次,HashSet 的 add 和 contains 平均都是 O (1)。
-
空间复杂度:O (n) 最坏情况下链表全是节点没有环,Set 要存下所有节点。
三、最优解法:快慢指针(Floyd 判圈算法)
1. 核心思想
想象一下操场跑圈:
- 一个人慢跑(slow),一个人快跑(fast)
- 如果跑道是直的(无环):跑得快的人会先跑到终点
- 如果跑道是环形的(有环):跑得快的人迟早会从后面追上慢跑的人
对应到链表:
slow每次走 1 步fast每次走 2 步- 如果有环:fast 一定会在环里追上 slow,两者相遇
- 如果无环:fast 会先走到 null
2. 完整 AC 代码
java
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针走一步
fast = fast.next.next; // 快指针走两步
if (slow == fast) { // 相遇 → 有环
return true;
}
}
// fast 走到终点 → 无环
return false;
}
}
3. 逐行拆解
① 初始化
ListNode slow = head;
ListNode fast = head;
快慢指针都从起点开始。
② 循环条件
while (fast != null && fast.next != null)
这是新手最容易写错的边界。 因为 fast 一次走两步,所以必须保证:
fast != null:当前快指针不是空fast.next != null:快指针的下一个也不是空 否则执行fast.next.next会空指针异常。
③ 移动指针
slow = slow.next;
fast = fast.next.next;
慢一步,快两步。 注意:先移动,再判断相遇,初始都在 head,不能一开始就判断相等。
④ 相遇判断
if (slow == fast) {
return true;
}
这里比较的是节点引用(地址),不是 val。 两个指针指向同一个节点对象,说明 fast 追上了 slow,存在环。
4. 为什么有环就一定能相遇?
核心是相对速度。 假设已经进入环中:
- slow 每次前进 1 步
- fast 每次前进 2 步
- 相当于 fast 相对于 slow,每次多走 1 步
假设两者在环中的距离是 k,那么每走一轮距离减少 1,最多 k 轮之后,fast 一定会追上 slow,两者相遇。
5. 为什么无环不会死循环?
如果链表没有环,fast 走得更快,一定会先到达链表末尾。 要么 fast == null,要么 fast.next == null,循环就会结束,返回 false。
四、新手必踩坑总结
-
HashSet 存 val 不存节点 值重复不代表有环,必须存节点对象本身。
-
快慢指针初始就判断相等 初始都指向 head,肯定相等,必须先移动再判断。
-
循环条件只写 fast != null 漏掉
fast.next != null,走两步的时候会空指针。 -
比较 slow.val == fast.val 不同节点可以有相同的值,必须比较节点引用本身。
五、两种解法对比
表格
| 解法 | 核心思想 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|---|
| HashSet | 记录访问过的节点,重复则有环 | O(n) | O(n) | 直观好理解,容易写 |
| 快慢指针 | Floyd 判圈,快追慢 | O(n) | O(1) | 空间最优,面试标准答法 |
六、面试口述标准回答
我首先想到的是用 HashSet 存储已经访问过的节点,遍历链表时如果遇到已经在集合中的节点,说明存在环。时间复杂度 O (n),空间复杂度 O (n)。
还可以进一步用快慢指针优化到 O (1) 空间。设置 slow 和 fast 两个指针,slow 每次走一步,fast 每次走两步。如果链表有环,fast 一定会在环内追上 slow;如果没有环,fast 会先到达链表末尾。时间复杂度仍然是 O (n),空间复杂度优化为常量级。
七、核心总结
这道题的核心可以浓缩成三句话:
- 直观思路用 Set,存节点不存值
- 最优解快慢指针,慢一步快两步
- 相遇即有环,快指针到终点即无环
HashSet 是保底思路,保证能写对;快慢指针是加分项,体现空间优化思维。面试时先讲直观思路,再讲优化解法,回答就非常完整了。