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

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

相关题目:

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. 大致解题思路:

给两个指针:

  • slow : 每次走一步
  • fast:每次走两步

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判圈法

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335