1. 程式人生 > >【演算法分析】如何理解快慢指標?判斷linked list中是否有環、找到環的起始節點位置。以Leetcode 141. Linked List Cycle, 142. Linked List Cycle II 為例Python實現

【演算法分析】如何理解快慢指標?判斷linked list中是否有環、找到環的起始節點位置。以Leetcode 141. Linked List Cycle, 142. Linked List Cycle II 為例Python實現

快慢指標簡述

快慢指標經常用於連結串列(linked list)中環(Cycle)相關的問題。

  1. 快指標(fast pointer)和慢指標(slow pointer)都從連結串列的head出發。
  2. slow pointer每次移動一格,而快指標每次移動兩格。
  3. 如果快慢指標能相遇,則證明連結串列中有環;否則沒有。

快慢指標的具體程式碼(C++, Python, Java版本)可以參考這個連結

LeetCode中對應題目分別是:

問題詳解 —— 為什麼如果有環,則快慢指標必定會相遇?

我們假設以下變數:

\(L_1\):起始節點(head node)到環起始節點(entry node)的距離。 \(C\)

: 環的長度。

假設我們的慢指標移動了\(x\)步,那麼快指標就移動了\(2x\)步。 那麼必定有\[(x-L_1)\% C= (2x-L_1) \% C \]\[(2x-x)\% C = 0\]\[x\%C=0\] 以上三個式子步步可逆,由於\(C\)是給定的fixed value,而\(x\)每步都在上升,因此必定有一個\(x=wC(w\in {N})\)使得他們相遇。並且有\(L_1 \le x\)所以必有\(x=\)\(\frac{L_1}{C}\)\(C\)為他們第一次相遇的地點。因此有\(x< L_1 + C\) where \(x=\)\(\frac{L_1}{C}\)\(C\)

這也就意味著他們相遇的地方一定是慢指標在環裡的第一圈。

問題詳解 —— 如何找到環的起始節點?

我們再增加一些變數: \(L_2\): 環起始節點(entry node)到快慢指標相遇節點的距離。 \(k\): 慢指標和快指標相遇的時候,慢指標走了的距離。 注意到,因為快指標走了的距離總是慢指標走了的距離的兩倍,因此\(2k\)是慢指標和快指標相遇的時候,快指標走了的距離。

由慢指標可以得出\[L_1+L_2=k\]由快指標可以得出,其中n是快指標已經走過的圈數\[L_1+nC+L_2 = 2k\]結合上述兩個式子,我們可以得出\[nC=k\] 當慢指標在遇到了快指標之後,慢指標又馬上移動了,那麼慢指標需要移動\(p\)

步後就可以讓慢指標就回到了環起始節點(entry node)。與此同時,在快慢指標相遇之後,又有一個指標馬上從原點出發,那麼它需要經過\(q\) 步才能到達起始節點(entry node)而且與慢指標相遇。 當慢指標和這個新指標相遇的時候,有\[(p-L_1)\%C=(q+L_2)\%C\]離散數學中的定理可知\[(p-q-L_1-L_2)\%C=0\]\[(p-q-nC)\%C=(p-q)\%C=0\] 不妨取\(p=q\)\(p-q=0\Rightarrow (p-q)\%C=0\) 因此當他們同時回到環起始節點(entry node)的時候,有慢指標第一次相遇後走過的距離\(p\)和新指標走的距離\(p=q\)。 值得注意的是\(L_2< C\),因此,他們第一相遇的時候必有\(q<C-L_2\),也就是說慢指標和新指標第一次相遇的時候,他們必定都在環起始節點(entry node)。

LeetCode 對應程式碼

判斷linked list中是否有環(141. Linked List Cycle)

這個演算法的時間複雜度是O(n)

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def hasCycle(self, head):
        """
        :type head: ListNode
        :rtype: bool
        """
        if head is None:
            return False
        slow = head
        fast = head
        while(fast.next and fast.next.next):
            slow = slow.next
            fast = fast.next.next
            if slow == fast:
                return True
        return False
        

找到環的起始節點(entry node)位置(142. Linked List Cycle II)

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def detectCycle(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        if head is None:
            return None
        slow, fast, new_node = head, head, head
        while(fast.next and fast.next.next):
            slow = slow.next
            fast = fast.next.next
            if slow == fast:
                while slow != new_node:
                    new_node = new_node.next
                    slow = slow.next
                return new_node      
        return None