1. 程式人生 > >尋找有環單鏈表的入口節點

尋找有環單鏈表的入口節點

問題陳述:一個可能存在環的單鏈表,尋找環的入口節點。

連結串列節點描述:

struct ListNode {
    int num;
    ListNode * next;
};

這一問題有許多解法,這裡說明三種『記數法』『斷鏈法』『差速法』,各有優劣。前兩種方法相對容易理解,第三種方法則相對優雅一些,建議直接看第三種方法。同時,這一問的演算法也可以用來判斷連結串列是否存在環。

一、記數法

這是最容易想到的方法,基於這樣一個既定的事實:點連結串列的環一定是單環(最簡單的那種),而且連結串列的尾節點一定指向環的入口節點。這樣的環只有三種形式:數字0型,數字6型,數字9型。 這樣看來,問題似乎就變成了尋找單鏈表的尾節點。然而可惜的是,由於環的尋在,尾節點已是不可尋了。
由上面的事實,我們還能得出另一個結論:只有環的入口節點有兩個指標指向它。 這便是記數法和斷鏈法的理論依據了。 所以技術法的思路就是:宣告一個字典,C++中為map,遍歷連結串列,記錄每個節點被指向的次數,一旦發現被指向了兩次的節點就返回該節點結束迴圈。 這各方法的缺點就是需要額外的儲存空間來儲存額外的資料,明顯的土豪做法。 參考程式碼:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
  if(!pHead) 
return null;
  map<ListNode*,int> flag;
  while(pHead) {
    if(++flag[pHead] == 2)
      return pHead;
    pHead=pHead->next;
  }
  return null;
}

二、斷鏈法

斷鏈法的依據也是連結串列中只有環的入口節點由兩個指標指向它。這樣,我們就可以遍歷連結串列,依次刪除節點之間的連線,直到遇到空指標,最後這個節點一定就是環的入口節點。

很明顯,這樣的做法會破環連結串列本身的結構,屬於傷敵一萬,自損八千的做法。

參考程式碼:

ListNode* EntryNodeOfLoop(ListNode* pHead) {
if(pHead == null|| pHead.next == null)
return null;
ListNode* fast=pHead->next;
ListNode* slow=pHead;
while(fast != null) {
slow->next = null;
slow = fast;
fast = fast->next;
}
return slow;
}
這個方法還有一個致命的缺點,就是當連結串列中沒有環的時候,它返回的是尾節點,但此時的尾節點並不是環的入口。

三、差速法

所謂的差速就是指兩個移動速度不一樣的快慢指標。在講述這個演算法之前,首先需要一點點的數學證明。


上圖是一個簡單有環單鏈表的模型圖,總長度為L,無環部分長度(圖中灰色部分)為m,環的長度(圖中黑色加橙色部分)為n,紅點為環的入口節點。 現在有兩個指標,一個每次移動一個節點,稱為慢指標,另一個的速度是前者的兩倍,稱為快指標。兩個指標同時從連結串列頭節點出發,它們必在環中相遇。圖中綠色節點為慢指標剛到達環入口節點時快指標的位置,此時距離慢指標的距離(圖中紅綠兩點間黑色部分)為x1,藍色點表示快慢指標在環中相遇的位置,此時距離環的入口節點距離(圖中紅藍兩點間黑色部分)為x,沿著運動方向距離入口節點距離(圖中橙色部分)還有y。 上面是對圖的說明,下面開始證明一個結論: m = k * n + y. (k為正整數) 當快慢指標相遇時,假設慢指標走了t步,那麼快指標一定走了2t步。 當慢指標進入以後,快指標一定會在慢指標走完一圈之前追上它。因為最壞的情況是慢指標進入環時快指標剛好落後它一整圈,這樣慢指標走完一圈快指標剛好追上慢指標。 快慢指標相遇時,慢指標走過的距離是: t = m + x 當慢指標剛進入環入口時,快指標走過的的距離是: t1 = m + k * n + x1. (k為正整數) 當快慢指標相遇時,快指標又走過了: t2 = n + x2 所以快慢指標相遇時,快指標一共走過了: t1 + t2 = m + k * n + x1 + n + x2 = m + (k + 1) * n + x = m + k * n + x. (k為正整數) 而相遇時快指標走過的距離是慢指標的兩倍。 2 * t = t1 + t2 2(m + x) = m + k * n + x m = k * n - x m = (k - 1) * n + (n - x) m = k * n + y.  (k為正整數) 這個結論說明,從頭節點到環入口的距離等於快慢指標相遇處繼續走到環入口(圖中橙色部分)的距離加上環長度的整倍數。如果有一個人從連結串列頭節點開始每次移動一個節點地往後走,另一個人從快慢指標相遇處(圖中藍色點)以同樣的速度往前走,結果就是,兩人相遇在環的入口節點處。 參考程式碼:
ListNode* EntryNodeOfLoop(ListNode* pHead) {
if(pHead == null|| pHead.next == null|| pHead.next.next == null)
return null;
ListNode* fast = pHead->next->next;
ListNode* slow = pHead->next;
while(fast! = slow) {
if(fast->next != null && fast->next->next != null) {
fast = fast->next->next;
slow = slow->next;
}
else{
return null;
        }
}
fast = pHead;
while(fast != slow){
fast = fast->next;
slow = slow->next;
}
 return slow;
}
結束。