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)檢測是否有環
基本思想:
這個可以用跑步來解釋,假設兩個人從同一起點出發(不從同一起點也可以),以不同的速度向前跑,最終快的人一定會追上慢的人(套圈)。可以將速度快的人換做兔子,速度慢的人換做烏龜,就變成龜兔賽跑了,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