環形鏈表1:
https://leetcode-cn.com/problems/linked-list-cycle/
方法一:哈希表
思路
我們可以通過檢查一個結點此前是否被訪問過來判斷鏈表是否爲環形鏈表。常用的方法是使用哈希表。
算法
我們遍歷所有結點並在哈希表中存儲每個結點的引用(或內存地址)。如果當前結點爲空結點 null(即已檢測到鏈表尾部的下一個結點),那麼我們已經遍歷完整個鏈表,並且該鏈表不是環形鏈表。如果當前結點的引用已經存在於哈希表中,那麼返回 true(即該鏈表爲環形鏈表)。
public boolean hasCycle(ListNode head) {
Set<ListNode> set= new HashSet<>();
while (head != null) {
if (set.contains(head)) {
return true;
} else {
set.add(head);
}
head = head.next;
}
return false;
}
複雜度分析
時間複雜度:O(n),對於含有 n個元素的鏈表,我們訪問每個元素最多一次。添加一個結點到哈希表中只需要花費 O(1) 的時間。
空間複雜度:O(n),空間取決於添加到哈希表中的元素數目,最多可以添加 n個元素。
方法二:雙指針
思路
想象一下,兩名運動員以不同的速度在環形賽道上跑步會發生什麼?
算法
通過使用具有 不同速度 的快、慢兩個指針遍歷鏈表,空間複雜度可以被降低至 O(1)。慢指針每次移動一步,而快指針每次移動兩步。
如果列表中不存在環,最終快指針將會最先到達尾部,此時我們可以返回 false。
現在考慮一個環形鏈表,把慢指針和快指針想象成兩個在環形賽道上跑步的運動員(分別稱之爲慢跑者與快跑者)。而快跑者最終一定會追上慢跑者。這是爲什麼呢?考慮下面這種情況(記作情況 A)- 假如快跑者只落後慢跑者一步,在下一次迭代中,它們就會分別跑了一步或兩步並相遇。
其他情況又會怎樣呢?例如,我們沒有考慮快跑者在慢跑者之後兩步或三步的情況。但其實不難想到,因爲在下一次或者下下次迭代後,又會變成上面提到的情況 A。
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
複雜度分析
時間複雜度:O(n),讓我們將 n 設爲鏈表中結點的總數。爲了分析時間複雜度,我們分別考慮下面兩種情況。
鏈表中不存在環:
快指針將會首先到達尾部,其時間取決於列表的長度,也就是 O(n)。
鏈表中存在環:
我們將慢指針的移動過程劃分爲兩個階段:非環部分與環形部分:
慢指針在走完非環部分階段後將進入環形部分:此時,快指針已經進入環中 \text{迭代次數} = \text{非環部分長度} = N迭代次數=非環部分長度=N
兩個指針都在環形區域中:考慮兩個在環形賽道上的運動員 - 快跑者每次移動兩步而慢跑者每次只移動一步。其速度的差值爲 1,因此需要經過 \dfrac{\text{二者之間距離}}{\text{速度差值}}
速度差值
二者之間距離
次循環後,快跑者可以追上慢跑者。這個距離幾乎就是 "\text{環形部分長度 K}環形部分長度 K" 且速度差值爲 1,我們得出這樣的結論 \text{迭代次數} = \text{近似於}迭代次數=近似於 "\text{環形部分長度 K}環形部分長度 K".
因此,在最糟糕的情形下,時間複雜度爲 O(N+K),也就是 O(n)。
空間複雜度:O(1),我們只使用了慢指針和快指針兩個結點,所以空間複雜度爲 O(1)。
環形鏈表2:
https://leetcode-cn.com/problems/linked-list-cycle-ii/
- 這類鏈表題目一般都是使用雙指針法解決的,例如尋找距離尾部第K個節點、尋找環入口、尋找公共尾部入口等。
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null){
return null;
}
//fast slow指針都從head節點開始
ListNode fast = head,slow = head;
/*查找第一次指針相遇的地方,若不是環則流程結束.
設非環部分長度爲a ,環部分長度爲b
第一次相遇時候 fast = slow + nb; fast = 2slow;
相減得: slow = nb; fast = 2nb;
*/
while(true){
//非環形鏈表,跳出
if(fast == null || fast.next == null){
return null;
}
fast = fast.next.next;
slow = slow.next;
//跳出循環的條件是此時快慢指針相遇
if(fast == slow){
break;
}
}
/*此時讓slow指針不動,fast從head開始。到達環入口部分滿足關係k = a + nb;
此時slow所走路程爲nb,再走a到達環入口部分,讓fast指針從head開始,到達相遇部分則爲環入口
部分*/
fast = head;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}