前言
链表环问题是链表经典高频面试题,分为两个基础题型:
hasCycle:判断单链表中是否存在环;detectCycle:若链表有环,返回环的起始节点,无环返回null。
单链表单向遍历,无法直接回头判断重复节点,业界主流两种解法:哈希集合法 (直观易写)、快慢指针(弗洛伊德判圈算法)(空间最优,面试核心考点)。
统一链表节点定义:
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
一、哈希集合解法(判环 + 找环起点通用)
核心思路
利用HashSet存储已经遍历过的节点对象(注意:存节点引用,不是节点值,不同节点允许值相同):
-
遍历链表指针依次向后移动;
-
每一步先判断当前节点是否在集合中:
- 存在:链表有环,当前节点就是环入口;
- 不存在:将当前节点存入集合,指针后移;
-
指针走到
null,说明链表无环。
优缺点
- 优点:逻辑极简,无需数学推导,判环、找环入口代码几乎一致,上手零门槛;
- 缺点:需要额外哈希集合存储所有节点,空间复杂度
O(n)。
1. 判断链表是否有环 hasCycle
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
ListNode p = head;
while (p != null) {
// 当前节点已访问过,存在环
if (set.contains(p)) {
return true;
}
set.add(p);
p = p.next;
}
// 遍历到末尾无环
return false;
}
}
2. 找到环的起始节点 detectCycle
public class Solution {
public ListNode detectCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
ListNode p = head;
while (p != null) {
// 重复节点即为环入口,直接返回
if (set.contains(p)) {
return p;
}
set.add(p);
p = p.next;
}
// 无环返回null
return null;
}
}
二、快慢指针(弗洛伊德判圈算法,空间 O (1) 最优解)
1. 快慢指针判断是否存在环 hasCycle
核心思路
快慢指针模拟跑道追及问题:
- 慢指针
slow:每次走 1 步; - 快指针
fast:每次走 2 步; - 若无环:快指针会率先走到链表末尾
null,循环终止返回false; - 若存在环:快慢指针一定会在环内某处相遇,相遇即证明有环。
边界提前特判:链表为空 / 只有单个节点,直接无环。
优缺点
- 优点:仅两个临时指针,空间复杂度
O(1),时间O(n),面试最优解法; - 缺点:需要理解追及逻辑,无法直接得到环入口,找起点需要额外推导。
完整代码
public class Solution {
public boolean hasCycle(ListNode head) {
// 空链表/单个节点一定无环
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
// 快慢指针相遇,存在环
if (fast == slow) {
return true;
}
}
// 快指针走到链表尾部,无环
return false;
}
}
补充:快慢指针寻找环入口(拓展考点)
数学推导核心结论
-
快慢指针第一次相遇后,将慢指针重置到链表头;
-
快慢指针同时改为每次走 1 步;
-
二者再次相遇的节点,就是环的起始节点。
推导本质:头部到环入口距离 = 相遇点到环入口的剩余环长。
detectCycle 快慢指针完整代码
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
// 第一步:快慢指针找相遇点
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
// 两指针相遇
if (slow == fast) {
// 慢指针放回头部
slow = head;
// 同步一步走,相遇即为环起点
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
// 无环
return null;
}
}
三、两种解法综合对比
| 方案 | 时间复杂度 | 空间复杂度 | 优势 | 短板 |
|---|---|---|---|---|
| 哈希集合 | O(n) | O(n) | 逻辑简单,可直接返回环入口 | 占用额外内存,长链表开销大 |
| 快慢指针 | O(n) | O(1) | 常数空间,性能最优 | 需要数学推导,找入口多一轮遍历 |
四、面试核心考点总结
-
哈希表注意点:存入节点引用而非数值,多个节点值相同但地址不同不算环;
-
快慢指针边界 :循环条件必须是
fast != null && fast.next != null,防止fast.next.next空指针报错; -
判环 vs 找入口区分:
- 只判断有无环:快慢指针相遇直接返回
true; - 寻找环起点:相遇后慢指针归头,同速前进二次相遇才是入口;
- 只判断有无环:快慢指针相遇直接返回
-
场景选择:刷题快速写答案用哈希;正式面试、追求最优空间复杂度必须掌握快慢指针弗洛伊德算法。