这道题我原来是不会的,然后自己思考了一下,发现其实是个数学问题,虽然方法用的是双指针,但是我还是从数学层面进行解释吧。
这道题是在141. Linked List Cycle基础上的变形题,那道题就是用双指针法,一个快指针fast每次走两步,一个慢指针slow每次走一步,如果快指针能和慢指针相遇,则说明存在环。
我们首先看一下有环的情况,例如下面这个链表
这个右边我是故意画成这种环状的,看着比较清晰,其实和原题的链表是一样的。
首先我们必须清楚一点,如果存在环,快指针和慢指针一定是在环中的某个结点位置相遇的,不可能在环之外的结点处相遇。
我们假设环开始的结点下标为i(下标从0开始),对于上图,i=2。
同时,我们设整个链表长度为len,对于上图,len=8。
那么我们很容易知道,环的长度为len-i,我们记环的长度为circle,即circle = len-i。
现在我们假设,当快指针和慢指针相遇的时候,慢指针一共走了n步。现在问题是,走了n步的慢指针,它现在所处结点的下标是多少?
不难算出,走了n步之后,下标为:
Xslow = i+(n-i)%circle
前提当然是n≥i,因为我们刚才说了,快慢指针相遇一定是在环内,所以n≥i必然成立。
慢指针走了n步,快指针则走了2n步,因为快指针速度是慢指针的2倍。
同理,走了2n步的快指针,它所在结点的下标为:
Xfast = i+(2n-i)%circle
现在两者相遇了,意思就是说 Xslow = Xfast
于是有:i+(n-i)%circle = i+(2n-i)%circle,即 (n-i)%circle = (2n-i)%circle。
这就是一个简单的同余方程,有无数个解,解为:
(2n-i) = (n-i) + k*circle, k=1,2,3...
但是注意,我们所说的相遇是第一次相遇,那么这个解就唯一了,即:
(2n-i) = (n-i) + circle
即,n = circle
没想到吧,结论竟是如此的简单!
不信,你看看上面那幅图,图中circle=6(即圈中结点的个数),那么快指针和慢指针第一次相遇时,慢指针一定是走了6步,此时慢指针到了下标为6的位置。
事实上快指针走了2*6=12步,刚好也到下标为6的结点上面了。
现在问题是,我在得到了这个相遇的结点的下标后,我怎么找到环中第一个结点呢?也就是下标为i的结点。
我们不要忘了,第一次相遇,快指针是比慢指针刚好多走了一圈,有人说你怎么知道是刚好多走一圈,因为刚才那个同余方程的解我们是令k=1得到的,k=1就是说多走了一圈。
假设从相遇结点到下标为i的结点需要y步,那么从小标为i结点到相遇结点自然要走circle-y步,于是
2n = i+circle+circle-y
n = i+circle-y
我们消掉其中的n,就会得到
i = y
i=y说明了什么?
说明了在找到相遇位置后,让一个指针从整个链表的头部出发,另外一个指针从相遇位置出发,两个指针每次走一步,当他们相遇的时候,就正好到了i号结点位置,i号结点,就是题目要求我们求的结点。
由于这里我们下标是从0开始编号的,所以head到i号结点的距离(所需要走的步数)也正好是i。
于是我们可以给出如下代码:
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) break;
}
if (!fast || !fast->next) return NULL;
slow = head;
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
return fast;
}
};