题目
标题和出处
标题:环形链表 II
出处:142. 环形链表 II
难度
2 级
题目描述
要求
给你一个链表的头结点 head \texttt{head} head,返回链表开始入环的结点。如果链表无环,则返回 null \texttt{null} null。
如果链表中有某个结点,可以通过连续跟踪 next \texttt{next} next 指针再次到达,则链表中存在环。评测系统内部使用 pos \texttt{pos} pos 表示链表尾结点的 next \texttt{next} next 指针连接到的结点下标(下标从 0 \texttt{0} 0 开始)。如果 pos \texttt{pos} pos 是 -1 \texttt{-1} -1,则在该链表中没有环。注意 pos \texttt{pos} pos 不作为参数进行传递。
不允许修改链表。
示例
示例 1:
输入:
head
=
[3,2,0,-4],
pos
=
1
\texttt{head = [3,2,0,-4], pos = 1}
head = [3,2,0,-4], pos = 1
输出:索引为
1
\texttt{1}
1 的链表结点
解释:链表中有一个环,其尾部连接到第
1
\texttt{1}
1 个结点。
示例 2:
输入:
head
=
[1,2],
pos
=
0
\texttt{head = [1,2], pos = 0}
head = [1,2], pos = 0
输出:索引为
0
\texttt{0}
0 的链表结点
解释:链表中有一个环,其尾部连接到第
0
\texttt{0}
0 个结点。
示例 3:
输入:
head
=
[1],
pos
=
-1
\texttt{head = [1], pos = -1}
head = [1], pos = -1
输出:
no
cycle
\texttt{no cycle}
no cycle
解释:链表中没有环。
数据范围
- 链表中结点的数目范围是 [0, 10 4 ] \texttt{[0, 10}^\texttt{4}\texttt{]} [0, 104]
- -10 5 ≤ Node.val ≤ 10 5 \texttt{-10}^\texttt{5} \le \texttt{Node.val} \le \texttt{10}^\texttt{5} -105≤Node.val≤105
- pos \texttt{pos} pos 为 -1 \texttt{-1} -1 或者链表中的一个有效下标
进阶
你是否可以使用 O(1) \texttt{O(1)} O(1) 空间解决此问题?
解法一
思路和算法
如果链表中存在环,则链表的尾结点的 next \textit{next} next 指针指向链表中的一个结点,被指向的结点为链表开始入环的结点。从链表的头结点 head \textit{head} head 开始遍历链表,如果链表存在环,则在访问尾结点之后会重复访问链表开始入环的结点,链表开始入环的结点是第一个被重复访问的结点。
如果链表中没有环,则任何结点都不会被重复访问。
因此可以遍历链表,寻找第一个被重复访问的结点。为了判断是否有结点被重复访问,可以使用哈希集合存储访问过的结点。
在遍历链表的过程中将访问到的每个结点加入哈希集合,遇到的第一个已经在哈希集合中的结点即为链表开始入环的结点,返回该结点。如果遍历链表结束到达 null \text{null} null 时仍没有遇到已经在哈希集合中的结点,则链表中没有环,返回 null \text{null} null。
代码
public class Solution {
public ListNode detectCycle(ListNode head) {
Set<ListNode> visited = new HashSet<ListNode>();
ListNode temp = head;
while (temp != null) {
if (!visited.add(temp)) {
return temp;
}
temp = temp.next;
}
return null;
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是链表的结点数。链表中的每个结点最多遍历一次。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是链表的结点数。需要使用哈希集合存储链表中的全部结点。
解法二
思路和算法
对于循环链表,另一个常用的方法是快慢指针。使用快慢指针寻找链表开始入环的结点分成两步。
第一步是使用快慢指针遍历链表并判断是否会相遇。
初始时,快指针和慢指针都位于链表的头结点。每次将快指针移动两步,慢指针移动一步,在至少移动一次的情况下,可能有两种情况:
-
如果快慢指针相遇,则链表中存在环,将快慢指针相遇的结点称为相遇点,从链表的头结点和相遇点开始遍历链表寻找链表开始入环的结点;
-
如果快指针到达链表末尾,则链表中不存在环,返回 null \text{null} null。
第二步是将两个指针分别从链表的头结点和相遇点开始遍历链表寻找链表开始入环的结点。
初始时,两个指针分别位于链表的头结点和相遇点。每次将两个指针各移动一步,两个指针相遇的结点即为链表开始入环的结点。
证明
假设链表入环之前的部分有 x x x 个结点(即从链表的头结点开始移动 x x x 步到达链表开始入环的结点),从链表开始入环的结点开始移动 y y y 步到达相遇点,从相遇点开始移动 z z z 步到达链表开始入环的结点,即环的长度是 y + z y + z y+z,其中 x x x、 y y y 和 z z z 都是非负整数且 y + z ≥ 1 y + z \ge 1 y+z≥1。如图所示。
当快慢指针相遇时,快慢指针都移动了 x + y x + y x+y 次,其中快指针移动了 2 ( x + y ) 2(x + y) 2(x+y) 步,慢指针移动了 x + y x + y x+y 步。假设此时快指针已经在环内移动了 k k k 整圈, k k k 是正整数,则有 2 ( x + y ) = x + k ( y + z ) + y 2(x + y) = x + k(y + z) + y 2(x+y)=x+k(y+z)+y,整理得 x = ( k − 1 ) ( y + z ) + z x = (k - 1)(y + z) + z x=(k−1)(y+z)+z。
由于 x x x 是链表入环之前的部分的结点数量,因此从链表的头结点开始移动 x x x 步到达链表开始入环的结点。
根据 x = ( k − 1 ) ( y + z ) + z x = (k - 1)(y + z) + z x=(k−1)(y+z)+z 可知,从相遇点开始移动 x x x 步即为在环内移动 k − 1 k - 1 k−1 整圈然后移动 z z z 步。由于从相遇点开始在环内移动 k − 1 k - 1 k−1 整圈之后回到相遇点,从相遇点开始移动 z z z 步到达链表开始入环的结点,因此从相遇点开始移动 x x x 步到达链表开始入环的结点。
由于从链表的头结点开始移动 x x x 步和从相遇点开始移动 x x x 步都到达链表开始入环的结点,因此上述做法中,位于链表的头结点和相遇点的两个指针同时移动 x x x 步之后同时到达链表开始入环的结点。
代码
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
ListNode meet = null;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
meet = fast;
break;
}
}
if (meet == null) {
return null;
}
ListNode pointer1 = head, pointer2 = meet;
while (pointer1 != pointer2) {
pointer1 = pointer1.next;
pointer2 = pointer2.next;
}
return pointer1;
}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是链表的结点数。第一次遍历到快慢指针相遇的移动次数不超过链表的结点数,第二次遍历到找到链表开始入环的结点的移动次数也不超过链表的结点数。
-
空间复杂度: O ( 1 ) O(1) O(1)。