141. 环形链表
问题描述
给定一个链表,判断链表中是否有环。如果链表中有某个节点可以通过连续跟踪 next
指针再次到达,则链表中存在环。使用整数 pos
表示链表尾连接到链表中的位置(索引从 0 开始),若 pos = -1
则无环。
示例:
输入:head = [3,2,0,-4], pos = 1 → 输出:true(尾部连接到第二个节点)
输入:head = [1,2], pos = 0 → 输出:true(尾部连接到第一个节点)
输入:head = [1], pos = -1 → 输出:false(无环)
算法思路:快慢指针(Floyd 判圈算法)
- 初始化指针:
slow
慢指针每次移动 1 步fast
快指针每次移动 2 步
- 遍历链表:
- 当
fast
不为null
且fast.next
不为null
时循环 - 移动指针:
slow = slow.next
,fast = fast.next.next
- 当
- 判断相遇:
- 若
slow == fast
说明有环 - 若
fast
遇到null
说明无环
- 若
数学证明:
- 设环外长度 ( a ),环内相遇点距环入口 ( b ),环剩余长度 ( c )(环总长 ( b+c ))
- 当
slow
走 ( a+b ) 步时,fast
走 ( 2(a+b) ) 步 - 快慢指针满足:( 2(a+b) = a + b + k(b+c) )(( k ) 为快指针绕环圈数)
- 化简得:( a = (k-1)(b+c) + c )
- 该等式恒成立,故必会相遇
代码实现
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
// 边界处理:空链表或单节点无环
if (head == null || head.next == null) {
return false;
}
ListNode slow = head; // 慢指针(每次1步)
ListNode fast = head; // 快指针(每次2步)
// 遍历链表(fast需检查两步)
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针移动1步
fast = fast.next.next; // 快指针移动2步
// 相遇说明有环
if (slow == fast) {
return true;
}
}
return false; // 遍历结束无环
}
}
算法分析
- 时间复杂度:( O(n) )
- 无环时:快指针先到达尾部,遍历 ( n/2 ) 次
- 有环时:慢指针入环前走 ( a ) 步,入环后走 ( c ) 步被追上(总步数小于 ( n ))
- 空间复杂度:( O(1) )
- 仅使用两个指针
关键点
-
终止条件:
fast == null
(偶数节点无环)fast.next == null
(奇数节点无环)
-
指针起点:
- 同起点
head
开始(也可让slow=head
,fast=head.next
)
- 同起点
-
移动顺序:
- 先移动指针再判断相遇(若先判断则初始状态
slow==fast
)
- 先移动指针再判断相遇(若先判断则初始状态
算法过程
无环链表:1 → 2 → 3 → null
步骤:
slow:1 → 2 → 3
fast:1 → 3 → null(终止)
有环链表:3 → 2 → 0 → -4 → 2(环)
步骤:
slow:3 → 2 → 0 → -4
fast:3 → 0 → 2 → -4 → 0 → 2
在 -4 处未相遇,在 2 处相遇
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:有环(尾部连接位置1)
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;
System.out.println("Test 1: " + solution.hasCycle(node1)); // true
// 测试用例2:有环(尾部连接位置0)
ListNode node5 = new ListNode(1);
ListNode node6 = new ListNode(2);
node5.next = node6; node6.next = node5;
System.out.println("Test 2: " + solution.hasCycle(node5)); // true
// 测试用例3:无环
ListNode node7 = new ListNode(1);
System.out.println("Test 3: " + solution.hasCycle(node7)); // false
// 测试用例4:长链表无环
ListNode head = new ListNode(0);
ListNode cur = head;
for (int i = 1; i <= 1000; i++) {
cur.next = new ListNode(i);
cur = cur.next;
}
System.out.println("Test 4: " + solution.hasCycle(head)); // false
}
常见问题
-
为什么快指针走
两步
?走三步可以吗?- 走两步必会相遇(速度差为1,能追上)。走三步可能错过(速度差为2,若环长
奇数
可能永远追不上)。
- 走两步必会相遇(速度差为1,能追上)。走三步可能错过(速度差为2,若环长
-
如何找到环的入口?
- 当快慢指针相遇后,将快指针移回链表头,然后两指针每次各走一步,再次相遇点即为环入口(根据公式 ( a = c ) 推导)。
-
边界条件处理
- 空链表直接返回
false
- 单节点需检查
next
是否为null
(无环)
- 空链表直接返回
-
为什么先移动指针再判断?
- 若先判断相遇,初始状态
slow
和fast
都指向head
会立即返回true
(错误判环)。
- 若先判断相遇,初始状态
142. 环形链表 II
问题描述
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。如果链表无环,则返回 null
。
示例:
输入:head = [3,2,0,-4],pos = 1(尾部连接到索引1节点)
输出:返回索引为1的节点(值为2)
算法思路:快慢指针 + 数学推导
- 判断是否有环:
- 快指针(每次2步)和慢指针(每次1步)从起点出发
- 若相遇则说明有环
- 寻找环入口:
- 相遇后将快指针移回链表头
- 两指针每次各走1步,再次相遇点即为环入口
数学证明:
- 设头节点到环入口距离为 ( a )
- 环入口到相遇点距离为 ( b )
- 相遇点到环入口距离为 ( c )(环长 ( L = b + c ))
- 第一次相遇时:
- 慢指针:( a + b )
- 快指针:( a + b + kL )(( k ) 为绕环圈数)
- 速度关系:( 2(a + b) = a + b + kL )
→ ( a + b = kL )
→ ( a = kL - b = (k-1)L + c ) - 表明:从链表头走 ( a ) 步 = 从相遇点走 ( c ) 步 + ( (k-1) ) 圈
代码实现
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
// 边界处理:空链表或单节点无环
if (head == null || head.next == null) {
return null;
}
ListNode slow = head; // 慢指针(每次1步)
ListNode fast = head; // 快指针(每次2步)
boolean hasCycle = false;
// 第一步:判断是否有环
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针移动1步
fast = fast.next.next; // 快指针移动2步
if (slow == fast) {
hasCycle = true;
break;
}
}
// 无环直接返回
if (!hasCycle) {
return null;
}
// 第二步:寻找环入口
fast = head; // 快指针移回链表头
while (slow != fast) {
slow = slow.next; // 两指针同步移动
fast = fast.next;
}
return slow; // 相遇点即为环入口
}
}
算法分析
- 时间复杂度:O(n)
- 第一轮找相遇点最多走 n 步
- 第二轮找入口最多走 n 步
- 空间复杂度:O(1)
- 仅使用两个指针
关键点
-
双指针初始位置:
- 同起点
head
开始 - 若从
slow=head
,fast=head.next
开始需调整相遇判断
- 同起点
-
移动规则:
- 第一轮:快2步/慢1步
- 第二轮:双指针均1步
-
数学关系:
- ( a = (k-1)L + c ) 是关键推导
- 环入口位置与圈数 ( k ) 无关
算法过程
a b
头节点 O → O → O → O ← 环入口
↑ ↓ c
O ← O 相遇点
步骤:
1. 快慢指针在相遇点相遇
2. 快指针移回头节点
3. 两指针各走 a 步:
- 快指针从头走 a 步到环入口
- 慢指针走 a = (k-1)L + c 步也到环入口
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 构造带环链表 [3,2,0,-4] (环入口在索引1)
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;
// 检测环入口
ListNode entry = solution.detectCycle(node1);
System.out.println("环入口值: " + (entry != null ? entry.val : "无环")); // 1
// 构造单节点环 [1] (尾部连自己)
ListNode node5 = new ListNode(1);
node5.next = node5;
entry = solution.detectCycle(node5);
System.out.println("环入口值: " + entry.val); // 0
// 构造无环链表 [1,2,3]
ListNode node6 = new ListNode(1);
node6.next = new ListNode(2);
node6.next.next = new ListNode(3);
entry = solution.detectCycle(node6);
System.out.println("环入口值: " + (entry != null ? entry.val : "无环")); // 无环
}
常见问题
-
为什么快指针走2步?走3步行不行?
- 走2步能保证数学关系成立(( a = (k-1)L + c ))。走3步可能导致无法推导出线性关系。
-
如何确保第二轮一定在入口相遇?
- 根据数学推导 ( a = (k-1)L + c ),当快指针走 ( a ) 步时,慢指针走 ( a ) 步相当于从相遇点走 ( c ) 步加 ( (k-1) ) 圈,正好到达入口。
-
若环很大或很小是否影响结果?
- 不影响。数学关系 ( a = (k-1)L + c ) 与环大小无关。
-
快指针能否初始化为
head.next
?- 可以,但需调整:
ListNode slow = head; ListNode fast = head.next; while (fast != slow) { if (fast == null || fast.next == null) return null; slow = slow.next; fast = fast.next.next; } // 后续相同
- 可以,但需调整: