LeetCode上面的題目#141:https://leetcode.com/problems/linked-list-cycle/。
題目大意:給定一個單鏈表,判斷其中是否有環。
這道題還有一個變種#142:https://leetcode.com/problems/linked-list-cycle-ii/。
題目大意:給定一個單鏈表,如果其中有環,輸出環的入口節點,否則輸出null。
對于這種題目,稍微有點經驗的人都會想到采用雙指針來解決,甚至LeetCode上有一個專門的topic,就叫做Two Pointers。用雙指針判斷迭代序列內是否存在環的算法即Floyd環檢測算法。
迭代序列中環的描述
令S為有限集合。如果有一個f為S映射到自身的函數,并且取任意一個a[0]∈S作為起始元素,那么序列A:
a[0], a[1]=f(a[0]), a[2]=f(a[1]), ..., a[i]=f(a[i-1]), ...
中一定存在環。
舉個例子,設S={0,1,2,3,4,5,6,7,8},f如下面的圖中所示。如果取a[0]=8或a[0]=2作為起始元素,那么產生的序列A中就會存在一個環6→3→1。
雖然上面畫成了有向圖的樣子,但圖論中的環與本文討論的環是兩碼事。圖論中檢測環的方法主要有DFS和拓撲排序,這里就不提了。
令μ為環的起始元素的最小下標(也就是環的入口點),λ為環的長度。Floyd環檢測算法的目的就是要在序列A中確定是否有環,如果有,就輸出μ和λ。
算法流程
又叫做“龜兔賽跑算法”。
- 判斷是否有環
使用兩個指針hare(兔子)和tortoise(烏龜),從序列的起始元素a[0]開始同步向后移動。hare每次移動兩個元素,tortoise每次移動一個元素。如果兩個指針在移動若干次后指向的元素值相同,那么序列中有環。
證明:如果有環存在的話,那么對于任意的i≥μ,都有a[i]=a[i+kλ],k是正整數。對于特定的k,總會存在一個i=kλ滿足條件,此時a[i]=a[2i]。所以可以利用步長分別為1和2的兩個指針來判斷。
- 確定λ和μ
一旦hare和tortoise指向的元素值相同,就將tortoise指針移回a[0]處,然后它們同步以步長1向后移動,直到它們指向的元素值再次相同。此時tortoise指向的位置就是μ,即環的入口點。
證明:如果在上面判斷有環的過程中,tortoise走過了距離d,那么hare就走過了距離2d,并且它們之間的差值正好是kλ。將tortoise重置后,如果要滿足a[j]=a[j+2d](j從0開始),根據定義,這個最小的j值也恰好就是μ。
然后,讓tortoise停在μ處不動,hare繼續以步長1向后移動,直到它們指向的元素值再次相同。此時hare走過的步數就是λ,即環的長度。
該算法的時間復雜度是O(μ+λ),空間復雜度是O(1)。
Linked List Cycle II的解法
單鏈表就是一種特殊的迭代序列,基于上面的思想就很容易寫出下面的代碼了。
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
slow = head;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
return null;
}
}