对于代码随想录中的这个解法(数学方法),给出我的理解与数学推导:
题目描述:
已知:
1.假设从头结点到环形入口节点的节点数为x,环形入口节点到fast指针与slow指针相遇节点的节点数为y,从相遇节点再到环形入口节点节点数为z。 也就是:头结点到环形节点入口的距离为,环形节点入口到fast与slow相遇节点(先假设他们一定相遇,之后进行进一步说明)的距离为,fast与slow相遇节点到环形入口节点的距离为。
2.fast每次移动两个节点,slow每次移动一个节点。也就是:fast的速度为,slow的速度为。
3.fast和slow同时从头结点出发。
求证:
1.fast和slow一定会相遇。(如果是环形链表)
2.fast和slow第一次相遇的时候,slow只经过一次环形入口节点(也就是没有走完1圈),fast经过过2-3次环形入口节点(也就是刚走完1圈、在第2圈或者第3圈)。
如图所示:
证明过程:
不妨把这个链表的环形部分理解成一个跑道?把讨论的起点变成从头结点变成环形入口节点。这样就能把这个看起来很复杂的问题简化成一个小学六年级的相遇/追及问题。
当slow到达环形入口的时候,时间过去了,此时fast已经走了,也就是在到达环形节点入口之后还走了。现在我们需要探讨的是与的大小关系。分为三类:、、。
情况1:
当slow到达环形入口时,fast往前走了,在此情况下fast还没有走完一圈,如图所示:
此时问题变成了一个追及问题:slow的速度为,fast的速度为,初始时刻(重新定义后),fast的位置是距离环形入口节点:,加上一圈的距离之后slow领先fast的距离就是:,请问fast在何时何地追上slow? 易知:经过之后fast追上slow,slow走了:(小于一圈的长度:),所以第一次相遇的时候slow只经过了一次环形入口节点,fast经过了两次环形入口节点。也就是相遇的时候slow一圈还没走完,fast已经走完了一整圈在走第2圈。
情况2:
同理可知,当slow到达环形入口时,fast往前走了,此时fast刚好走过一圈来到了环形入口节点与slow相遇。所以第一次相遇的时候slow只经过了一次环形入口节点,fast经过了两次环形入口节点。也就是相遇的时候slow一圈还没开始走,fast已经走完了一整圈在走第2圈。初始情况如图所示:
情况3:
同理可知,当slow到达环形入口时,fast往前走了,此时fast已经走完了一圈多,(因为fast走完一圈来到环形入口节点的时候slow还没到达环形入口,所以没有相遇过)。此时fast的位置在环形入口节点往前:,加上一圈的距离之后slow领先fast:。初始情况如图所示:
易知:经过,fast和slow相遇,slow已经走了(大于还是小于一圈的距离:呢?)
使用作差法,,可以知道,也就是第一次相遇的时候slow并没有走完一圈。所以第一次相遇的时候slow只经过了一次环形入口节点,fast经过了三次环形入口节点。也就是相遇的时候slow一圈还没走完,fast已经走完了两整圈在走第3圈。
综上:
对于求证1,因为我们已经把这道题转换成了三种情况下的追及问题,不管哪种情况下fast和slow都会相遇,所以得证(原谅不够抽象,没有从形式上实现数学证明一般的严谨)
对于求证2,综合三种情况,我们可以知道:fast和slow第一次相遇的时候,slow只经过一次环形入口节点(也就是没有走完1圈)(三种情况都是),fast经过过2-3次环形入口节点(也就是第2圈或者第3圈)。(第一种情况在走第2圈,第二种情况刚走完1圈,第三种情况在走第3圈)
为什么要证明这些呢?
首先是为代码中的「判断是否存在环」部分提供进一步的支撑,减少看完题解之后不知所以然的内心的不安:
// 判断是否存在环
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) { // 如果快慢指针相遇
isExistCycle = true; // 说明存在环
break; // 别忘了跳出循环
}
}
第二是对于代码随想录给出的解答中的这部分给出了进一步的说明:
为什么slow走过的节点数不需要加呢?,fast走过的节点数中的n的取值到底是多少呢?根据上面的证明可以知道n=1或者2以及这两种情况对应的大小关系。
至于之后的代码逻辑——放置两个指针于头结点以及相遇的地方,来定位环形入口的位置,只能说太妙,但是根据上面三种情况也很容易推理证明出来,由于笔者懒惰本篇文章就不再赘述。
else { // 如果存在环
ListNode *pointer1 = head;
ListNode *pointer2 = fast; // 此逻辑很复杂,之后会写题解
while (pointer1 != pointer2) {
pointer1 = pointer1->next;
pointer2 = pointer2->next;
} // pointer1和pointer2相遇,跳出循环
return pointer1; // 相遇的结点即环的入口
}