环形链表II——数学方法的进一步理解

161 阅读5分钟

对于代码随想录中的这个解法(数学方法),给出我的理解与数学推导:

题目描述:

已知:

1.假设从头结点到环形入口节点的节点数为x,环形入口节点到fast指针与slow指针相遇节点的节点数为y,从相遇节点再到环形入口节点节点数为z。 也就是:头结点到环形节点入口的距离为xx,环形节点入口到fast与slow相遇节点(先假设他们一定相遇,之后进行进一步说明)的距离为yy,fast与slow相遇节点到环形入口节点的距离为zz

2.fast每次移动两个节点,slow每次移动一个节点。也就是:fast的速度为2v2v,slow的速度为vv

3.fast和slow同时从头结点出发。

求证:

1.fast和slow一定会相遇。(如果是环形链表)

2.fast和slow第一次相遇的时候,slow只经过一次环形入口节点(也就是没有走完1圈),fast经过过2-3次环形入口节点(也就是刚走完1圈、在第2圈或者第3圈)。

如图所示:

20220925103433.png

证明过程:

不妨把这个链表的环形部分理解成一个跑道?把讨论的起点变成从头结点变成环形入口节点。这样就能把这个看起来很复杂的问题简化成一个小学六年级的相遇/追及问题。

当slow到达环形入口的时候,时间过去了xv\frac{x}{v},此时fast已经走了2v×xv=2x2v \times \frac{x}{v} = 2x,也就是在到达环形节点入口之后还走了xx。现在我们需要探讨的是y+zy+zx\Large x的大小关系。分为三类:x<y+zx < y+zx=y+zx = y+zx>y+zx > y+z

情况1:x<y+zx < y+z

当slow到达环形入口时,fast往前走了xx,在此情况下fast还没有走完一圈,如图所示: 未命名文件.jpg

此时问题变成了一个追及问题:slow的速度为v\Large v,fast的速度为2v2v,初始时刻(重新定义后),fast的位置是距离环形入口节点:xx,加上一圈的距离之后slow领先fast的距离就是:y+zxy+z-x,请问fast在何时何地追上slow? 易知:经过t=y+zx2vv=y+zxvt = \frac{y+z-x}{2v-v} = \frac{y+z-x}{v}之后fast追上slow,slow走了:y+zxy+z-x(小于一圈的长度:y+zy+z),所以第一次相遇的时候slow只经过了一次环形入口节点,fast经过了两次环形入口节点。也就是相遇的时候slow一圈还没走完,fast已经走完了一整圈在走第2圈。

情况2:x=y+zx = y+z

同理可知,当slow到达环形入口时,fast往前走了x=y+zx=y+z,此时fast刚好走过一圈来到了环形入口节点与slow相遇。所以第一次相遇的时候slow只经过了一次环形入口节点,fast经过了两次环形入口节点。也就是相遇的时候slow一圈还没开始走,fast已经走完了一整圈在走第2圈。初始情况如图所示: 未命名文件.jpg

情况3:x>y+zx > y+z

同理可知,当slow到达环形入口时,fast往前走了x>y+zx>y+z,此时fast已经走完了一圈多,(因为fast走完一圈来到环形入口节点的时候slow还没到达环形入口,所以没有相遇过)。此时fast的位置在环形入口节点往前:xyzx-y-z,加上一圈的距离之后slow领先fast:y+z(xyz)=2y+2zxy+z-(x-y-z) = 2y+2z-x。初始情况如图所示: 未命名文件.jpg

易知:经过t=2y+2zx2vv=2y+2zxvt = \frac{2y+2z-x}{2v-v} = \frac{2y+2z-x}{v},fast和slow相遇,slow已经走了(2y+2zxv)×v=2y+2zx\left(\frac{2y+2z-x}{v}\right) \times v = 2y + 2z -x(大于还是小于一圈的距离:y+zy+z呢?)

使用作差法,(2y+2zx)(y+z)=y+zx<0(2y+ 2z -x ) - (y+z) = y+z-x < 0,可以知道2y+2zx<y+z2y+2z-x < y+z,也就是第一次相遇的时候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; // 别忘了跳出循环
            }
        }

第二是对于代码随想录给出的解答中的这部分给出了进一步的说明: 截屏.jpg 为什么slow走过的节点数不需要加n(y+z)n(y+z)呢?,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; // 相遇的结点即环的入口
        }