LeetCode链接:142. 环形链表 II - 力扣(LeetCode)
目录
1、我们设置了slow和fast两枚指针,其中fast走的比slow快,但是他们一定会相遇吗?
一、Set
使用集合看能不能找到之前走过的Node,这是一种取巧的解决方法,在此给出相关代码,但不作为本文分析的重点。
public ListNode detectCycle(ListNode head) {
Set<ListNode> set = new HashSet<>();
while (head != null) {
if (set.contains(head)) {
return head;
}
set.add(head);
head = head.next;
}
return null;
}
二、快慢指针
这是关于这个问题的常规解决思路,我之前在完成这道题的过程中,对于算法是知其然不知其所以然的。这一次重新刷LeetCode的时候,决定把每一个细节都搞懂。让我们从一个一个的基本设定开始吧。
0、如果不存在环
如果不存在环,那么fast一定会先到达null,至此算法结束。
以下讨论基于存在环的情况,对其中的细节问题进行分析。
1、我们设置了slow和fast两枚指针,其中fast走的比slow快,但是他们一定会相遇吗?
我们忽略一切特殊的设定,例如fast的速度是slow的整数倍,仅考虑最基本的情况,V(fast)>V(slow)。
假设fast的速度(每步走的距离)是f,slow的速度是s,假设存在环,其长度为c。
①fast先入环。
基于最基本的设定,fast的速度大于slow,那么当slow到达环的入口时,fast一定已经在环内了。注意,即使此时他们就相遇了,但“环的入口”也属于“环内”。
②一定会相遇吗?
我们考虑一种最普遍的情况,slow入环在某个位置,此时fast已在环内(因为fast先入环),slow在fast“前方”(从它们运动的方向考虑)距离fast的距离是L>0。
使用假设法解决这个问题,假设fast和slow会相遇,意味着方程tf-ts=mc+L有解。其中t是从此时刻开始的时间,取值为正整数,m是相遇时fast多走的圈数,取值为非负整数。
这个方程不一定有解!
考虑以下情况:c=12,L=11,f=6,s=4。在这种情况下,fast和slow在任意时刻的位置都是确定的集合,且两个集合无交集,这意味着它们经过任意圈任意时间,都不可能会相遇。
如下图所示。
2、为什么fast速度设置为2,slow速度设置为1?
我们在上面讨论了,如果我们任意设置fast和slow的速度,仅仅考虑f>s,他们是可能永远不会相遇的。相遇的本质是,fast出现的位置集合和slow出现的位置集合存在交集,然后经过n圈之后,fast和slow在交集处相遇。
所以,为了使fast和slow相遇,从而通过相遇点和他们的速度这些信息,建立环入口和链表起点及相遇点之间的距离关系,我们需要通过设置速度,使得它们一定能相遇且距离关系建立的足够简单清晰。
所以,我们设置了fast速度为2,slow速度为1。
①保证一定相遇
当fast或者slow走的越慢,他们位置的集合越大,从而二者的位置交集就越大,相遇机会就越大。
当f=2且s=1时,二者一定会相遇。
初始时slow领先fast L,每一步之后距离缩短1,经过L步之后二者就一定会相遇。
如果f-s是2或者3或者其他数,由于L%(f-s)不一定为正整数,这就使得可能需要经过若干圈fast和slow才会相遇(不利于距离关系的建立),甚至二者根本不会再相遇。
②一定在slow的第一圈相遇
设置fast速度为2而slow速度为1的另一个好处是保证二者一定在slow进入环的第一圈就相遇,这有助于建立清晰的距离关系。
slow从进入环到绕环一周,需要的时间是c(环的长度);
fast先到环的入口(即slow进入环的地方),再去追击,再到达环的入口的所需时间是(c+L)/2。
1 如果L=c,意味着slow刚进入环时slow和fast就相遇了,显然这是在slow的第一圈相遇
2 如果0<L<c,那么(c+L)/2<c。则fast必定比slow先到达(或直接超过)环的入口,又因为fast与slow必定相遇,则fast必会在slow完成第一圈之前就与其相遇。
最极端的情况,即L取最大值c-1的时候,slow进入环时fast刚好在它前面一步,如上面的图所示。在这种情况下,fast将会在slow第一圈的最后一步(即再走一步就到环入口的位置)追上并与slow相遇。
证明:在这种情况下,slow走c-1步,到达环入口前一步,需要的时间是c-1;fast走L+c-1步,即c-1+c-1=2(c-1)步,需要的时间也是c-1。这意味着,经过相等的时间fast和slow出现在同一位置,即二者相遇,且这个位置在slow的第一圈之内。
至于中间情况,即L<c-1的情况,fast会更早的追上并与slow相遇,因为他们之间的距离更短,这意味着slow能走的距离也更短。
更简单的说明:fast追上slow时一定与其相遇,这个时间是L(因为每一步他们之间的距离缩短1)。而L的最大取值是c-1,即即使fast追上slow花费最长的时间,slow在这段时间里也只能跑到距离入口一步的地方,无法完成一圈!
3、距离关系
head到环的入口的距离是x,环的入口到相遇的地方的距离是y,相遇的地方到环的入口的距离是z
fast走过x+y+n(y+z),slow走过x+y。因为slow没有足够的时间绕环就被追上了。
则x+y+n(y+z) = 2(x+y),x=(n-1)(y+z)+z。
因此,此时让新指针p从head出发,q从相遇点出发,二者都以每次一步的速度走,二者相遇的地方即为环的入口。其中n-1表示q在遇到p之前多绕的圈数。
4、代码:
package LinkedList;
// 13 https://leetcode.cn/problems/linked-list-cycle-ii/description/
/**
* fast每次走两步,slow每次走一步,如果fast和slow相遇,说明有环,否则没有环
* <p>
* 在有环的情况下:
* head到环的入口的距离是x,环的入口到相遇的地方的距离是y,相遇的地方到环的入口的距离是z
* 快指针走过的距离是x+y+n(y+z),慢指针走过的距离是x+y,所以x+y+n(y+z) = 2(x+y)
* 所以x=(n-1)(y+z)+z
* 此时让新指针p从head出发,q从相遇点出发,二者都以每次一步的速度走,二者相遇的地方即为环的入口
* 其中n-1表示q在遇到p之前多绕的圈数
*/
public class CircularLinkedListII {
// public ListNode detectCycle(ListNode head) {
// Set<ListNode> set = new HashSet<>();
// while (head != null) {
// if (set.contains(head)) {
// return head;
// }
// set.add(head);
// head = head.next;
// }
// return null;
// }
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head, fast = head;
do {
if (fast.next == null || fast.next.next == null) {
return null;
} else {
fast = fast.next.next;
slow = slow.next;
}
} while (fast != slow);
ListNode p = head, q = slow;
while (p != q) {
p = p.next;
q = q.next;
}
return p;
}
}
这样对这个问题就完成了充分的说明,主要的细节是:
1、为什么fast速度设置为2而slow速度设置为1:保证相遇且有利于建立清晰的距离关系
2、为什么一定在slow的第一圈相遇:因为时间不够slow完成一圈就被fast追上了。保证能够在slow的第一圈相遇就是建立清晰的距离关系的原因。