如何判斷單鏈表是否存在環
有一個單向鏈表,鏈表當中有可能出現“環”,就像題圖這樣。如何用程序判斷出這個鏈表是有環鏈表?
不允許修改鏈表結構。
時間復雜度O(n),空間復雜度O(1)。
方法一、窮舉遍歷
方法一:首先從頭節點開始,依次遍歷單鏈表的每一個節點。每遍歷到一個新節點,就從頭節點重新遍歷新節點之前的所有節點,用新節點ID和此節點之前所有節點ID依次作比較。如果發現新節點之前的所有節點當中存在相同節點ID,則說明該節點被遍歷過兩次,鏈表有環;如果之前的所有節點當中不存在相同的節點,就繼續遍歷下一個新節點,繼續重復剛才的操作。
例如這樣的鏈表:A->B->C->D->B->C->D, 當遍歷到節點D的時候,我們需要比較的是之前的節點A、B、C,不存在相同節點。這時候要遍歷的下一個新節點是B,B之前的節點A、B、C、D中恰好也存在B,因此B出現了兩次,判斷出鏈表有環。
假設從鏈表頭節點到入環點的距離是D,鏈表的環長是S。那么算法的時間復雜度是0+1+2+3+…+(D+S-1) = (D+S-1)*(D+S)/2 , 可以簡單地理解成 O(N*N)。而此算法沒有創建額外存儲空間,空間復雜度可以簡單地理解成為O(1)。
方法二、哈希表緩存
****首先創建一個以節點ID為鍵的HashSet集合,用來存儲曾經遍歷過的節點。然后同樣是從頭節點開始,依次遍歷單鏈表的每一個節點。每遍歷到一個新節點,就用新節點和HashSet集合當中存儲的節點作比較,如果發現HashSet當中存在相同節點ID,則說明鏈表有環,如果HashSet當中不存在相同的節點ID,就把這個新節點ID存入HashSet,之后進入下一節點,繼續重復剛才的操作。
這個方法在流程上和方法一類似,本質的區別是使用了HashSet作為額外的緩存。
假設從鏈表頭節點到入環點的距離是D,鏈表的環長是S。而每一次HashSet查找元素的時間復雜度是O(1), 所以總體的時間復雜度是1*(D+S)=D+S,可以簡單理解為O(N)。而算法的空間復雜度還是D+S-1,可以簡單地理解成O(N)。
方法三、快慢指針
首先創建兩個指針1和2(在java里就是兩個對象引用),同時指向這個鏈表的頭節點。然后開始一個大循環,在循環體中,讓指針1每次向下移動一個節點,讓指針2每次向下移動兩個節點,然后比較兩個指針指向的節點是否相同。如果相同,則判斷出鏈表有環,如果不同,則繼續下一次循環。
例如鏈表A->B->C->D->B->C->D,兩個指針最初都指向節點A,進入第一輪循環,指針1移動到了節點B,指針2移動到了C。第二輪循環,指針1移動到了節點C,指針2移動到了節點B。第三輪循環,指針1移動到了節點D,指針2移動到了節點D,此時兩指針指向同一節點,判斷出鏈表有環。
此方法也可以用一個更生動的例子來形容:在一個環形跑道上,兩個運動員在同一地點起跑,一個運動員速度快,一個運動員速度慢。當兩人跑了一段時間,速度快的運動員必然會從速度慢的運動員身后再次追上並超過,原因很簡單,因為跑道是環形的。
/**
* 判斷單鏈表是否存在環
* @param head
* @return
*/
public static <T> boolean isLoopList(ListNode<T> head){
ListNode<T> slowPointer, fastPointer;
//使用快慢指針,慢指針每次向前一步,快指針每次兩步
slowPointer = fastPointer = head;
while(fastPointer != null && fastPointer.next != null){
slowPointer = slowPointer.next;
fastPointer = fastPointer.next.next;
//兩指針相遇則有環
if(slowPointer == fastPointer){
return true;
}
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
假設從鏈表頭節點到入環點的距離是D,鏈表的環長是S。那么循環會進行S次(為什么是S次,有心的同學可以自己揣摩下),可以簡單理解為O(N)。除了兩個指針以外,沒有使用任何額外存儲空間,所以空間復雜度是O(1)。
方法四、Set集合大小變化
評論中有 @長沙小辣椒 同學指出:還可以用 set 遍歷鏈表,把節點放入set里,每次訪問下個節點時,如果set長度不變,則跳出,說明有環。否則set長度+1,繼續遍歷。
該方法時間復雜度是O(N),空間復雜度上因為需要額外等數量的存儲空間,所以空間復雜度是O(n)。
如何找出有環鏈表的入環點?
根據這篇文章:鏈表中環形的入口,我們來分析一下入環口和我們上面這個快慢指針相遇點的關系。
當fast若與slow相遇時,slow肯定沒有走遍歷完鏈表(不是一整個環,有開頭部分,如上圖)或者恰好遍歷一圈(未做驗證,看我的表格例子,在1處相遇)。於是我們從鏈表頭、相遇點分別設一個指針,每次各走一步,兩個指針必定相遇,且相遇第一點為環入口點(慢指針走了n步,第一次相遇在c點,對慢指針來說n=s+p,也就是說如果慢指針從c點再走n步,又會到c點,那么順時針的CB距離是n-p=s,但是我們不知道s是幾,那么當快指針此時在A點一步一步走,當快慢指針相遇時,相遇點恰好是圓環七點B(AB=CB=s))。
/**
* 找到有環鏈表的入口
* @param head
* @return
*/
public static <T> ListNode<T> findEntranceInLoopList(ListNode<T> head){
ListNode<T> slowPointer, fastPointer;
//使用快慢指針,慢指針每次向前一步,快指針每次兩步
boolean isLoop = false;
slowPointer = fastPointer = head;
while(fastPointer != null && fastPointer.next != null){
slowPointer = slowPointer.next;
fastPointer = fastPointer.next.next;
//兩指針相遇則有環
if(slowPointer == fastPointer){
isLoop = true;
break;
}
}
//一個指針從鏈表頭開始,一個從相遇點開始,每次一步,再次相遇的點即是入口節點
if(isLoop){
slowPointer = head;
while(fastPointer != null && fastPointer.next != null){
//兩指針相遇的點即是入口節點
if(slowPointer == fastPointer){
return slowPointer;
}
slowPointer = slowPointer.next;
fastPointer = fastPointer.next;
}
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
如何判斷兩個單鏈表是否相交,以及相交點
方法一、直接法
直接判斷第一個鏈表的每個結點是否在第二個鏈表中,時間復雜度為O(len1*len2),耗時很大
方法二、利用計數
如果兩個鏈表相交,則兩個鏈表就會有共同的結點;而結點地址又是結點唯一標識。因而判斷兩個鏈表中是否存在地址一致的節點,就可以知道是否相交了。可以對第一 個鏈表的節點地址進行hash排序,建立hash表,然后針對第二個鏈表的每個節點的地址查詢hash表,如果它在hash表中出現,則說明兩個鏈表有共 同的結點。這個方法的時間復雜度為:O(max(len1+len2);但同時還得增加O(len1)的存儲空間存儲哈希表。這樣減少了時間復雜度,增加 了存儲空間。
以鏈表節點地址為值,遍歷第一個鏈表,使用Hash保存所有節點地址值,結束條件為到最后一個節點(無環)或Hash中該地址值已經存在(有環)。
再遍歷第二個鏈表,判斷節點地址值是否已經存在於上面創建的Hash表中。
這個方面可以解決題目中的所有情況,時間復雜度為O(m+n),m和n分別是兩個鏈表中節點數量。由於節點地址指針就是一個整型,假設鏈表都是在堆中動態創建的,可以使用堆的起始地址作為偏移量,以地址減去這個偏移量作為Hash函數
方法三、利用有環鏈表思路
對於兩個沒有環的鏈表相交於一節點,則在這個節點之后的所有結點都是兩個鏈表所共有的。如果它們相交,則最后一個結點一定是共有的,則只需要判斷最后一個結點是否相同即可。時間復雜度為O(len1+len2)。對於相交的第一個結點,則可求出兩個鏈表的長度,然后用長的減去短的得到一個差值 K,然后讓長的鏈表先遍歷K個結點,然后兩個鏈表再開始比較。
還可以這樣:其中一個鏈表首尾相連,檢測另外一個鏈表是否存在環,如果存在,則兩個鏈表相交,而檢測出來的依賴環入口即為相交的第一個
參考資料
漫畫算法:如何判斷鏈表有環?
判斷兩個單鏈表是否相交
數據結構面試 之 單鏈表是否有環及環入口點 附有最詳細明了的圖解
鏈表中環形的入口
【單鏈表】環的入口點 原理理解!
如何判斷單鏈表是否有環、環的入口、環的長度和總長