关于单链表查环及其相关问题的数学证明(最完全最清晰)

2020-07-14  本文已影响0人  培根炒蛋

未经许可严禁转载,侵权必究

相关题目:

141. Linked List Cycle
142. Linked List Cycle II

这道题的具体描述和解题代码就不多做解释了,写关于解法的多如牛毛,下功夫都能明白这道题应该都能清楚是怎么做的。

顺便说一下,这种找重复的问题首先要想到的是要用HashSet去解决,O(n)的方法如果你没见过的话是很难想出来的。

但是最关键问题在于,解法虽然简单,但是怎么证明你的解法是对的,除了leetcode的黑盒测试,给出一个数学上的一般化证明是十分必要。

我看了网上的很多资料,绝大多数给出的证明都是错的,只是瞎猫碰死耗子碰对了,要么剩下的就极其繁琐文字众多并且一些地方非常模糊。

不想看描述和解法的可以直接跳过下面的部分。

描述

Given a linked list, determine if it has a cycle in it.

To represent a cycle in the given linked list, we use an integer pos which represents the position (0-indexed) in the linked list where tail connects to. If pos is -1, then there is no cycle in the linked list.

Example 1:

Input: head = [3,2,0,-4], pos = 1
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the second node.

image

Example 2:

Input: head = [1,2], pos = 0
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the first node.

image

Example 3:

Input: head = [1], pos = -1
Output: false
Explanation: There is no cycle in the linked list.

image

141 解:

public class Solution {
    public boolean hasCycle(ListNode head) {
            ListNode slow = head, fast = head;
            while (slow != null) {
                if (fast.next == null || fast.next.next == null) {
                    break;
                }
                slow = slow.next;
                fast = fast.next.next;
                if (slow == fast) {
                    return true;
                }
            }
            return false;
    }
}

142 解:

public class Solution {
    private ListNode getIntersect(ListNode head) {
        ListNode tortoise = head;
        ListNode hare = head;

        // A fast pointer will either loop around a cycle and meet the slow
        // pointer or reach the `null` at the end of a non-cyclic list.
        while (hare != null && hare.next != null) {
            tortoise = tortoise.next;
            hare = hare.next.next;
            if (tortoise == hare) {
                return tortoise;
            }
        }

        return null;
}

    public ListNode detectCycle(ListNode head) {
        if (head == null) {
            return null;
        }

        // If there is a cycle, the fast/slow pointers will intersect at some
        // node. Otherwise, there is no cycle, so we cannot find an entrance to
        // a cycle.
        ListNode intersect = getIntersect(head);
        if (intersect == null) {
            return null;
        }

        // To find the entrance to the cycle, we have two pointers traverse at
        // the same speed -- one from the front of the list, and the other from
        // the point of intersection.
        ListNode ptr1 = head;
        ListNode ptr2 = intersect;
        while (ptr1 != ptr2) {
            ptr1 = ptr1.next;
            ptr2 = ptr2.next;
        }

        return ptr1;
    }
}

1. 大致解题思路:

给两个指针:

slowfast指针从头开始扫描链表。指针 slow 每次走1步,指针 fast 每次走2步。如果存在环,则指针 slowfast会相遇。

如果环不存在,fast遇到NULL退出。

使用这种方法,有一个定律

fastslow一定会环内相遇。

我们这篇文章的主要目的就在于给出这个定律的证明。

2. 最广泛的错误证法:

如果两个指针的速度不一样,比如pq(0<p<q)二者满足什么样的关系,可以使得两者肯定交与一个节点?

p为指针slow每步移动距离,q为指针fast的每步移动距离。
设当slow指针到环的入口时,fast指针已经在环里走过了k

Sp(i) = p*i

  1. 如果两个要相交于一个节点,则
    Sq(i) = k + q*i

Sp(i) = Sq(i)

(p*i) \bmod n = ( k+ q*i ) \bmod n

[ (q - p) * i + k ] \bmod n =0

(q-p)i + k = N*n [N 为自然数]

i = (N*n -k) /(p-q)
i取自然数,则当 pq满足上面等式 即存在一个自然数N,可以满足N*n - kp - q 的倍数时,保证两者相交。

这个在互联网上最为广泛简洁的证明看似很有道理没什么问题,但是却有其致命的错误:

求模操作满足分配率吗?

我不会证求模操作是否满足分配率,但是运行如下代码(python),可以证明上面证明步骤 4->5 是错误的:

ks = range(1,100,1)
ns = range(1,100,1)

total = len(ks) * len(ks) * len(ns)
i = 0
for n in ns:
    for k1 in ks:
        for k2 in ks:
            if (k1 + k2) % n != k1 % n + k2 % n:
                i += 1
                #print('k1 : {} k2 : {} n :{}'.format(k1,k2,n))
print('unsatisfied : {}'.format(i))
print('total : {}'.format(total))

输出结果:

unsatisfied : 393883
total : 970299

实际上求模操作是不满足像除法一样的分配率的

当然上面的错误也归功于百度百科给出的错误公式。
(a+b) \mod p = ( a \mod p + b \mod p )
实际上求模的分配率是这样的
(a + b) \bmod n = [(a \bmod n) + (b \bmod n)] \bmod n

同样给出一段代码:

ks = range(1,100,1)
ns = range(1,100,1)

i = 0

for n in ns:
    for k1 in ks:
        for k2 in ks:
            if (k1 + k2) % n != (k1 % n + k2 % n) % n:
                i+=1

print('unsatisfied : {}'.format(i))
print('total : {}'.format(total))

输出结果:

unsatisfied : 0
total : 970299

3. 正确的证法:

设快慢指针slowfast

设从开始走过的总步数为s

设非环部分的长度为n_1,环的长度为n_2

  1. 假设当slow进入环,从开始走过s步相遇有:
    (s-n_1) \bmod n_2 = (2*s - n_1) \bmod n_2

  2. 即两者余数相同时等式成立,设余数为c,设slow 指针在环中走过了k_1圈,fast指针在环中走过了k_2圈,可得

s - n_1 = k_1 * n_2 + c

2*s - n_1 =k_2 * n_2 + c

  1. 推出
    s = (k_2 - k_1) * n_2

上面这个式子明显是成立的,因为 k_2 - k_1 >=0k_2 >= k_1
这里很明显k_2 > k_1

又因s >= n_1

所以当sN * n_2N为正整数时

5 式成立


k_1 =0

s = k_2 * n_2
6 式成立,并能使k_2最小。

至于k_2是多少,具体要取决于链表中非环和环的长度之比,因为s >= n_1\exists k_2 \in N使得k_2 * n_2 >= n_1时 6 式成立。

从而可以得出推论,
k_2 >= \frac{n_1}{n_2}

n_1 <= n_2k_2 >=1

n_1 > n_2k_2 >=2

根据以上的证明,关于单链表查环相关的问题都可以迎刃而解。


4. 求环的长度

slowfast相遇后,slow再次与fast相遇,计算slow走过的步数就是环的长度。

太过浅显请读者自证。


5. 求环的入口

这里我们可以利用上面快慢指针相遇的结论,假设同上:

根据上面的结论,假设当slowfast相遇时,相遇点为h,其中h是环内的点0 <= h < n_2

  1. 当两个指针相遇时可以得到
    2 * distance(slow) = distance(fast)

2 * (n_1 + h) = n_1 + k_2 * n_2 + h

  1. 化简可得
    n_1 - k_2 * n_2 + h = 0
  2. 等式两边同时 mod n_2
    [n_1 \bmod n_2 + h \bmod n_2] \bmod n_2 = 0
  3. 当且仅当下式满足时等式成立
    n1 = k*n_2 + c
  4. c为余数,可得
    c+h = t * n_2
  5. 可得
    c = t * n_2 - h
  6. 最终得到结论
    n_1 = (k + t-1) * n_2 + n_2 - h

这样我们可以得到一个结论,我们设定一个头指针,从head 开始走,在slowfast指针第一次相交的位置,slow指针开始走,当head指针走到环的入口,即slow指针在环里走了n_2 - h 步,head指针与slow指针相交,找到环的入口。


最后只附上一个英文资料,是leetcode美国版上的,他的证明虽然对,但是有点太魔幻,不是很容易理解:
https://leetcode.com/articles/linked-list-cycle-ii/

这个单链表查环问题的问题有个专有名称:Floyd's Tortoise and Hare或者Floyd判圈法

因为我看到的中文资料几乎全部有问题,当然也不排除我的做法本身也有问题,因为我的数学功底本身就很弱,如果是行家的话应该能从我的过程看的出来,这就麻烦一些精通数学证明的朋友们来帮忙指正了。

上一篇下一篇

猜你喜欢

热点阅读