判斷單鏈表中是否有環,找到環的入口節點
聲明
- 文章可以隨意轉載,但請注明出處。
- 文中有一些地方引用了其他文章,但都已標明出處。如有侵犯,可立即刪除。
- 文中有些地方並無冒犯之意,希望提及的博客作者理解。沒有你們的幫助,對這個問題毫無頭緒。
- 由於CSDN博客系統的內部錯誤,所有的公式后面都有一條惱人的豎線,實屬無奈。
- 歡迎評論。
文章梗概
本文通過對現有資料的收集和整理,給出了一種相對簡單的嚴格證明的“判斷單鏈表是否有環,找到環的入口節點”的方法。
題目描述
一個鏈表中包含環,請找出該鏈表的環的入口結點(牛客網題目鏈接),題目中沒有說是單鏈表,從給出的代碼中可以看出是單鏈表而且不可修改鏈表元素的定義。
思考過程
從兩個大的角度思考這個問題
1.記錄遇到的每一個鏈表元素
在一次遍歷過程中的,使用一種數據結構(數組、Hash表、基數樹)記錄遇到的每一個鏈表元素並判斷是否已經遇到過,其中使用基數樹可以獲得O(n)
的時間復雜度,但是可會有比較高的空間復雜度。
2.利用鏈表的性質
- 在題目的討論頁看到了比較有想法的一個答案冬至_的答案,正如其所言時間復雜度為O(n),兩個指針,一個在前面,另一個緊鄰着這個指針,在后面。兩個指針同時向前移動,每移動一次,前面的指針的next指向NULL。也就是說:訪問過的節點都斷開,最后到達的那個節點一定是尾節點的下一個,也就是循環的第一個。這時候已經是第二次訪問循環的第一節點了,第一次訪問的時候我們已經讓它指向了NULL,所以到這結束。這樣做的話過題目是可以了,但是會破壞掉原來的鏈表,所以並不是一個特別完美的解決辦法。如果可以在鏈表元素中加入一個記錄“邏輯斷開”的元素,也就是說在遍歷的過程中,不真正的斷開元素之間的連接,而是使用一個記錄值,記錄下“邏輯上的斷開”。
- 另一個答案頁上比較正統的回答為0909的回答,其主要思想和蒙恩的罪人的新浪博客大同小異,優雅簡潔,后面主要針對蒙恩的罪人的新浪博客進行討論。
相關問題的解法與證明
給定一個鏈表,只給出頭指針h
1.如何判斷是否存在環?
使用追趕的方法,設定兩個指針slow、fast
,均從頭指針開始,每次分別前進1步、2步。如存在環,則兩者相遇;如不存在環,fast遇到NULL
退出。其中主要的思想就是“環形相遇追及問題”,理解上應該不復雜。
2.如何知道環的長度?
記錄下問題1的相遇點,slow、fast
從該點開始,再次相遇時slow所經過的節點數就是環的長度。從環上的任意一點開始,slow、fast再次相遇時slow經過的節點數就是環的長度,因為此時slow、fast起始距離為環長,速度差為1
。選擇問題1的相遇點為起始點是為了確保起始點為環上的一點。
3.如何找出環的連接點在哪里?
設問題1中的相遇點為m1
,賦值p=m1,q=h,其中h為鏈表頭結點,然后p,q每次1步向前運動,p,q
再次相遇所在的位置就是環的入口節點(環的連接點)。這里和上面提到的博客中的敘述差別非常大,這也是其有些問題的地方,我在這里更正了其說法,並給出了相對嚴格的證明。
相對簡潔的實現
public class Solution { ListNode EntryNodeOfLoop(ListNode h){ if(h == null || h.next == null) return null; ListNode slow = h; ListNode fast = h; while(fast != null && fast.next != null ){ slow = slow.next; fast = fast.next.next; if(slow == fast){ ListNode p=h; ListNode q=slow;//相當於讓q指向了m1 while(p != q){ p = p.next; q = q.next; } if(p == q) return q; } } return null; }
代碼及問題三的證明
我們把該鏈表抽象為這樣一個模型,假設環長為n
。
情景1
此圖表示其實狀態,其中h
表示鏈表頭節點,slow,fast,起始狀態指向h,t
表示環的入口節點。
情景2
此圖表示slow
運動到了t,fast運動到m1,節點h和節點t之間的距離為a,節點t和節點m1之間的距離(弧長)為b,並設此時fast在環上做了r次圓周運動(因為a和n的長度都不固定,多以fast可能已經在環上運動了好多圈了)。相對於slow其運動的距離為:a由於fast速度是slow的二倍,所以其運動的距離(步數)為:2a
並且,經過觀察可知fast運動的距離為:a+nr+b
,所以可知公式①:
情景3.
此時,slow,fast
相遇在節點m2,也就是代碼中10行判斷成立的地方。其中m2為相遇點,b還是為弧長。由於鏈表的指針是有方向的,我們約定在環上計算距離的時候按照逆時針計算,也就是說,從t到m1的距離為b,從m1到t的距離為n−b(其中n為環的長度)。
同理在情況2中,從fast到slow的距離為n−b,它們的速度差為1,所以它們再次相遇的時候經過的時間為n−b1=n−b,slow經過的距離為(n−b)×1=n−b,所以假設相遇點為m2,那么顯而m2到t的距離為b
。
情景4.
情況4對應着代碼中的11
~19行。因為通過上面的討論,如果能讓q向前運動:b+xn步,那么q的位置恰好是t,其中x∈{0,1,2,3,⋅⋅⋅}。
值得高興的是,在情況2中我們有公式①,觀察到a恰好符合這樣一個步數值,所以我們讓p=h,p,q,都每次向前移動1,當他們相遇的時候恰好就是環的入點t,也就是說p從h移動到p,q再次相遇在這里的作用是提供一個計數。
所以,當p,q再次相遇的時候,他們的相遇點恰好了t
,也就是需要找的環的入口點。
復雜度
我們關注第一次循環的slow
和第二次循環的p,因為它們都是每次前進一步,由它們移動的步數,可以得到算法的時間復雜度,所以易知時間復雜度為O(n),空間復雜度為O(1)。