上手快慢指針,看着一篇就夠了


這篇博客討論一下常見的快慢指針算法

緣起

自從刷leetcode以來,已經碰見過很多次應用快慢指針算法的題目了,但是每次都是直接刷過,並沒有好好思考爲什麼要這麼做,這篇博文來探討一下快慢指針一些常見的問題,瞭解這些問題可能對你刷題並沒有很大的幫助,但是會讓你對快慢指針瞭解的更加深刻。

什麼是快慢指針

快慢指針中的快慢指的是移動的步長,即每次向前移動速度的快慢。例如可以讓快指針每次沿鏈表向前移動2,慢指針每次向前移動1次。
--------來自百度百科

上述描述了快慢指針算法的基本內容,其實瞭解這些東西,再看一個例題,差不多就可以開始利用快慢指針來解題了,但是本文探討的是另外的問題。先來看個題理解一下快慢指針算法吧。

快慢指針的一類常見應用就是判斷鏈表是不是含有環,如:141.環形鏈表。基本思路就是快慢指針遍歷,如果相遇就表示有環,反之則沒有。基本代碼如下:

bool hasCycle(struct ListNode *head) {
    if(NULL == head)
        return false;

    struct ListNode *fast = head;
    struct ListNode *slow = head;

    while(slow && fast && fast->next){
        slow = slow->next;		    // 慢指針每次走一步
        fast = fast->next->next;	// 快指針每次走兩步
        if(slow == fast)
            return true;
    }
    return false;
}

本文核心

問題一:爲什麼快指針每次移動2,慢指針每次移動1?

我看過的幾乎所有的快慢指針的題解都是快指針每次移動2,慢指針每次移動1,這樣毫無疑問是正確的,但是爲什麼呢?能不能快指針每次移動3呢?首先來分析一下快指針移動2,慢指針移動1的情況,

情況一:環爲奇數

在這裏插入圖片描述
很明顯,這個鏈表有環,分析一下slow和fast的指向的結點

slow:0、1、2、3、4、5、6、7
fast: 0、2、4、6、1、3、5、7

根據上面的值,兩者在結點7時相遇,正好是慢指針轉一圈(第一圈還沒轉完,但是爲了便於描述採用這種方式),快指針轉兩圈的時候。

情況二:環爲偶數

在這裏插入圖片描述
同樣看一下slow和fast的值

slow:0、1、2、3、4、5、6
fast: 0、2、4、6、2、4、6

同樣是快指針轉兩圈,慢指針轉一圈。當然以上兩種情況並不完整,但是已經足以說明問題。有興趣的可以試試快慢指針都從環的入口結點進入環的情況,對應上圖就是slow和fast第一個值都是1的情況。

回到正題,爲什麼會出現如上的情況呢,都是快指針轉兩圈慢指針轉一圈的時候相遇呢?下面從數學方面解釋一下:

假設鏈表無環部分結點有 m 個,環的結點個數爲 n 個。慢指針走了 t 步之後與快指針相遇,那麼慢指針在環中走的步數爲 t - m ,快指針爲 2t - m。此時快、慢指針必然是在環中;假設是在環入口後的第 x 個結點相遇,慢指針走了k1圈,快指針走了k2圈。則有

t - m = k1 * n + x
2t - m = k2 * n + x

加減消元求出 t = (k2 - k1) * n;也就是說慢指針走過n步後與快指針第一次相遇。這部分內容參考:證明 :快慢指針可以判斷單鏈表是否有環

再來看看快指針每次走三步,慢指針每次走一步的情況,以下面這個圖爲例:
在這裏插入圖片描述
slow:0、1、2、3、4、5、6、7
fast: 0、3、6、2、5、1、4、7

可以看出,仍然是慢指針經過 n 步之後與快指針第一次相遇,但是這時候的快指針已經走了三圈。可以驗證快指針走四步,慢指針走一步的情況,還是會相遇,但是快指針轉了四圈了。

從上面的分析可以看出,快指針可以不是2,但是爲什麼大家都喜歡寫2呢?

先把上面的代碼copy下來

bool hasCycle(struct ListNode *head) {
    if(NULL == head)
        return false;

    struct ListNode *fast = head;
    struct ListNode *slow = head;

    while(slow && fast && fast->next){
        slow = slow->next;		// 慢指針每次走一步
        fast = fast->next->next;	// 快指針每次走兩步
        if(slow == fast)
            return true;
    }
    return false;
}

我猜測是這樣的,當選擇快指針每次移動三個單位時,上面while循環的條件判斷會增加,不然很可能(這裏我想說一定的,但是有些特例不會)會報空指針錯誤。因此快指針每次移動2,慢指針移動1是一種比較好的思路。另外不要以爲每次都是在環的最後一個結點相遇。

問題二:如何判斷環的入口結點和環大小

從上面問題一的討論可以看出,取快指針爲2,慢指針爲1是比較好判斷鏈表是否有環的方法。下面需要討論的是,在鏈表有環的基礎上如何找到環的入口。假設有如下鏈表。
在這裏插入圖片描述
先看一下slow和fast的值,slow和fast會在哪個結點相遇

slow:1、2、3、4、5、6、7
fast: 1、3、5、7、3、5、7

ok,slow和fast會在結點7相遇,而且很容易得到,下一次相遇也是在7這個結點。假設slow經過 t 步之後到達7,那麼fast經過了 2t 步。而且可以肯定slow再經過 t 步之後還是在結點7這個位置,爲啥?(t + t = 2t)。現在將fast重新移動到head處,即結點1所在位置,slow仍然指向結點7,然後slow和fast每次都前進一步(也就是fast和slow步長一樣了),那麼他們相遇的第一個結點就是環的入口。爲什麼?

上面已經說過了,slow是經過了 t 步之後到達7,並且slow再經過 t 步之後仍然是結點7。將fast移動到head之後,每次前進1,t 步之後會和slow重逢(相當於一開始的slow)。來看看slow和fast的路徑:

slow:7、8、3、4、5、6、7
fast: 1、2、3、4、5、6、7

注意到了嗎,3、4、5、6、7這一串是公共的路徑,因此 3 就是環的入口結點。

分析到這一步之後,如果要求環的大小也很容易了。不用fast指向head,直接從第一次相遇的位置,fast和slow指向不變,fast每次移動2,slow每次移動1,設置個count,計算下次相遇經過了多少結點就可以了。

結束語

到了這裏,本文基本結束了,但是並不意味着這是快慢指針的全部內容,只是博主僅僅見識到了這些、僅僅想到了這些內容罷了,如果大家關於快慢指針還有啥有意思的問題,或者本文未曾提到的知識,歡迎留言和私信。我會盡量弄明白,並且按照情況更新到這篇文章中。
helping others is improving myself

reference

leetcode – 287. 尋找重複數 | 陳牧遠的回答
證明 :快慢指針可以判斷單鏈表是否有環
快慢指針尋找環入口——學習筆記
快慢指針判斷單向鏈表是否有環及找環入口
141. 環形鏈表

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章