1. 程式人生 > >Floyd's Cycle Detection Algorithm

Floyd's Cycle Detection Algorithm

Floyd判圈演算法


1. 什麼是Floyd判圈演算法?

Floyd判圈演算法(Floyd Cycle Detection Algorithm),又稱龜兔賽跑演算法(Tortoise and Hare Algorithm),是一個可以在有限狀態機、迭代函式或者連結串列上判斷是否存在環,求出該環的起點與長度的演算法。


2. 演算法描述

如果有限狀態機、迭代函式或者連結串列上存在環,那麼在某個環上以不同速度前進的2個指標必定會在某個時刻相遇。同時顯然地,如果從同一個起點(即使這個起點不在某個環上)同時開始以不同速度前進的2個指標最終相遇,那麼可以判定存在一個環,且可以求出2者相遇處所在的環的起點與長度。

Floyd Cycle Detection Algorithm主要解決三個問題:

  1. 檢測是否有環;
  2. 如果有環,求環的起點;
  3. 如果有環,求環的長度;

(1)檢測是否有環

基本思想:
這個可以用跑步來解釋,假設兩個人從同一起點出發(不從同一起點也可以),以不同的速度向前跑,最終快的人一定會追上慢的人(套圈)。可以將速度快的人換做兔子,速度慢的人換做烏龜,就變成龜兔賽跑了,23333…….

基於上述思想,我們可以這樣檢測是否有環:
初始狀態下,假設起點為S。現設兩個指標t和h,將它們均指向S。接著,讓t和h同時以不同的速度向前推進:t速度為v,h速度為2v。當h無法向前推進時,即可確定沒有環;如果t與h相遇,則可以確定有環。(注意,起點不一定在環上。)

(2)如果有環,求環的起點;

基本思想:
在上述演算法判斷出存在環時,顯然t和h在同一位置。此時,只要令h仍位於原來的位置M,而令t返回起點S,此時h與t之間距為環C長度的整數倍。隨後,同時讓t和h以相同的速度往前推進:即t每前進1步,h前進1步。持續該過程直至t與h再一次相遇,此相遇地點即為環C的一個起點P。

很多小夥伴看到這兒會很困惑,為毛是這樣呢?我們可以用圖來解釋一下。
這裡寫圖片描述

已經確定有環,設起點到環的起點距離為m,環的周長為n,第一次相遇時距離環的起點的距離為k,第一次相遇時慢指標在環上轉了a圈,快指標在環上轉了b圈。(這裡假定h的速度是t速度的2倍)
兩者第一次相遇時,慢指標移動的距離i為: i = m + a*n + k; 快指標速度是慢指標速度的2倍,故快指標移動的距離2i為: 2i = m + b*n + k。兩者相減得,i = (b-a)*n,即i是環長度的倍數。此時,按上述演算法,令慢指標返回起點,兩個指標均以慢指標的速度同時向前推進。當慢指標推進m時,會到達環的起點,此時快指標移動的總距離為 2i+m。考慮這個 2i+m,可以理解為從起點走m,到達環起點,然後走了整數倍的環長度,故最終快指標也會到達環起點(即快慢指指標在環起點相遇)。

(3)如果有環,求環的長度;

基本思想:
這個相對來說比較簡單。只需要在快慢指標相遇時,保持一個指標不動,讓另外一個指標向前推進,記錄其步數。當兩個指標再次相遇時,第二個指標推進的步數,即為環的長度。


3. 演算法實現

這裡引用一個維基上的python的演算法實現,其他語言的也類似。

def floyd(f, x0):
    # Main phase of algorithm: finding a repetition x_i = x_2i.
    # The hare moves twice as quickly as the tortoise and
    # the distance between them increases by 1 at each step.
    # Eventually they will both be inside the cycle and then,
    # at some point, the distance between them will be
    # divisible by the period λ.
    tortoise = f(x0) # f(x0) is the element/node next to x0.
    hare = f(f(x0))
    while tortoise != hare:
        tortoise = f(tortoise)
        hare = f(f(hare))

    # At this point the tortoise position, ν, which is also equal
    # to the distance between hare and tortoise, is divisible by
    # the period λ. So hare moving in circle one step at a time, 
    # and tortoise (reset to x0) moving towards the circle, will 
    # intersect at the beginning of the circle. Because the 
    # distance between them is constant at 2ν, a multiple of λ,
    # they will agree as soon as the tortoise reaches index μ.

    # Find the position μ of first repetition.    
    mu = 0
    tortoise = x0
    while tortoise != hare:
        tortoise = f(tortoise)
        hare = f(hare)   # Hare and tortoise move at same speed
        mu += 1

    # Find the length of the shortest cycle starting from x_μ
    # The hare moves one step at a time while tortoise is still.
    # lam is incremented until λ is found.
    lam = 1
    hare = f(tortoise)
    while tortoise != hare:
        hare = f(hare)
        lam += 1

    return lam, mu

4. 演算法複雜度

時間複雜度:

注意到當指標t到達環C的起點時(此時指標h顯然在環C上),之後指標t最多僅可能走1圈。若設起點S到環起點P距離為 m,環C的長度為 n,則時間複雜度為O(m+n),是線性時間的演算法。

空間複雜度:

僅需要創立指標t、指標h,儲存環長n、環的一個起點P。空間複雜度為 O(1),是常數空間的演算法。

最後,說一下,大多數情況下,判斷是否有環的問題,也可以用HashSet來實現,即每次用HashSet記錄出現過的節點,當一個節點重複出現時,即可判斷存在環。HashSet這種方法的空間複雜度為O(n), 故從空間複雜度的角度考慮,Floyd判圈演算法要優於HashSet這種方法。


參考資料:
1. https://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare
2. https://zh.wikipedia.org/wiki/Floyd%E5%88%A4%E5%9C%88%E7%AE%97%E6%B3%95
3. https://stackoverflow.com/questions/2936213/explain-how-finding-cycle-start-node-in-cycle-linked-list-work