關(guān)于單鏈表查環(huán)及其相關(guān)問題的數(shù)學(xué)證明(最完全最清晰)

未經(jīng)許可嚴禁轉(zhuǎn)載,侵權(quán)必究

相關(guān)題目:

141. Linked List Cycle
142. Linked List Cycle II

這道題的具體描述和解題代碼就不多做解釋了,寫關(guān)于解法的多如牛毛,下功夫都能明白這道題應(yīng)該都能清楚是怎么做的。

順便說一下,這種找重復(fù)的問題首先要想到的是要用HashSet去解決,O(n)的方法如果你沒見過的話是很難想出來的。

但是最關(guān)鍵問題在于,解法雖然簡單,但是怎么證明你的解法是對的,除了leetcode的黑盒測試,給出一個數(shù)學(xué)上的一般化證明是十分必要。

我看了網(wǎng)上的很多資料,絕大多數(shù)給出的證明都是錯的,只是瞎貓碰死耗子碰對了,要么剩下的就極其繁瑣文字眾多并且一些地方非常模糊。

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

描述

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:每次走兩步

slow,fast指針從頭開始掃描鏈表。指針 slow 每次走1步,指針 fast 每次走2步。如果存在環(huán),則指針 slow 、fast會相遇。

如果環(huán)不存在,fast遇到NULL退出。

使用這種方法,有一個定律

fastslow一定會環(huán)內(nèi)相遇。

我們這篇文章的主要目的就在于給出這個定律的證明。

2. 最廣泛的錯誤證法:

如果兩個指針的速度不一樣,比如pq,(0<p<q)二者滿足什么樣的關(guān)系,可以使得兩者肯定交與一個節(jié)點?

設(shè)p為指針slow每步移動距離,q為指針fast的每步移動距離。
設(shè)當slow指針到環(huán)的入口時,fast指針已經(jīng)在環(huán)里走過了k

Sp(i) = p*i

  1. 如果兩個要相交于一個節(jié)點,則
    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 為自然數(shù)]

i = (N*n -k) /(p-q)
i取自然數(shù),則當 p,q滿足上面等式 即存在一個自然數(shù)N,可以滿足N*n - kp - q 的倍數(shù)時,保證兩者相交。

這個在互聯(lián)網(wǎng)上最為廣泛簡潔的證明看似很有道理沒什么問題,但是卻有其致命的錯誤:

求模操作滿足分配率嗎?

我不會證求模操作是否滿足分配率,但是運行如下代碼(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))

輸出結(jié)果:

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))

輸出結(jié)果:

unsatisfied : 0
total : 970299

3. 正確的證法:

設(shè)快慢指針slow,fast

設(shè)從開始走過的總步數(shù)為s

設(shè)非環(huán)部分的長度為n_1,環(huán)的長度為n_2

  1. 假設(shè)當slow進入環(huán),從開始走過s步相遇有:
    (s-n_1) \bmod n_2 = (2*s - n_1) \bmod n_2

  2. 即兩者余數(shù)相同時等式成立,設(shè)余數(shù)為c,設(shè)slow 指針在環(huán)中走過了k_1圈,fast指針在環(huán)中走過了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_2,N為正整數(shù)時

5 式成立


k_1 =0

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

至于k_2是多少,具體要取決于鏈表中非環(huán)和環(huán)的長度之比,因為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

根據(jù)以上的證明,關(guān)于單鏈表查環(huán)相關(guān)的問題都可以迎刃而解。


4. 求環(huán)的長度

slowfast相遇后,slow再次與fast相遇,計算slow走過的步數(shù)就是環(huán)的長度。

太過淺顯請讀者自證。


5. 求環(huán)的入口

這里我們可以利用上面快慢指針相遇的結(jié)論,假設(shè)同上:

根據(jù)上面的結(jié)論,假設(shè)當slowfast相遇時,相遇點為h,其中h是環(huán)內(nèi)的點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為余數(shù),可得
    c+h = t * n_2
  5. 可得
    c = t * n_2 - h
  6. 最終得到結(jié)論
    n_1 = (k + t-1) * n_2 + n_2 - h

這樣我們可以得到一個結(jié)論,我們設(shè)定一個頭指針,從head 開始走,在slowfast指針第一次相交的位置,slow指針開始走,當head指針走到環(huán)的入口,即slow指針在環(huán)里走了n_2 - h 步,head指針與slow指針相交,找到環(huán)的入口。


最后只附上一個英文資料,是leetcode美國版上的,他的證明雖然對,但是有點太魔幻,不是很容易理解:
https://leetcode.com/articles/linked-list-cycle-ii/

這個單鏈表查環(huán)問題的問題有個專有名稱:Floyd's Tortoise and Hare或者Floyd判圈法

因為我看到的中文資料幾乎全部有問題,當然也不排除我的做法本身也有問題,因為我的數(shù)學(xué)功底本身就很弱,如果是行家的話應(yīng)該能從我的過程看的出來,這就麻煩一些精通數(shù)學(xué)證明的朋友們來幫忙指正了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容