雙指標技巧秒殺七道連結串列題目(完)
雙指標技巧秒殺七道連結串列題目(2)
單鏈表的倒數第 k 個節點
從前往後尋找單鏈表的第 k
個節點很簡單,一個 for 迴圈遍歷過去就找到了,但是如何尋找從後往前數的第 k
個節點呢?
這個解法就比較巧妙了,假設 k = 2
,思路如下:
首先,我們先讓一個指標 p1
指向連結串列的頭節點 head
,然後走 k
步:
現在的 p1
,只要再走 n - k
步,就能走到連結串列末尾的空指標了對吧?
趁這個時候,再用一個指標 p2
指向連結串列頭節點 head
:
接下來就很顯然了,讓 p1
和 p2
同時向前走,p1
走到連結串列末尾的空指標時前進了 n - k
步,p2
也從 head
開始前進了 n - k
n - k + 1
個節點上,即恰好停連結串列的倒數第 k
個節點上:
這樣,只遍歷了一次連結串列,就獲得了倒數第 k
個節點 p2
。
例如:
力扣第 19 題「 刪除連結串列的倒數第 N 個結點」
我的程式碼:
class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { //虛擬頭節點 ListNode* dummy=new ListNode(-1); dummy->next=head; ListNode* fast=dummy; ListNode* slow=dummy; //要刪除第n個,就要先找到第n+1個節點,所以fast先走n+1步 for(int i=0;i<=n;i++){ fast=fast->next; } while(fast!=nullptr){ fast=fast->next; slow=slow->next; } slow->next=slow->next->next; return dummy->next; } };
注意:
要使用虛擬頭節點,這樣可以省去討論許多特殊情況,返回dummy->next,head可以為空。
單鏈表的中點
力扣第 876 題「 連結串列的中間結點」就是這個題目,問題的關鍵也在於我們無法直接得到單鏈表的長度 n
,常規方法也是先遍歷連結串列計算 n
,再遍歷一次得到第 n / 2
個節點,也就是中間節點。
我們讓兩個指標 slow
和 fast
分別指向連結串列頭結點 head
。
每當慢指標 slow
前進一步,快指標 fast
就前進兩步,這樣,當 fast
走到連結串列末尾時,slow
就指向了連結串列中點。
我的程式碼:
class Solution { public: ListNode* middleNode(ListNode* head) { //返回中間節點,利用快慢指標即可,快的指標每次走2步,慢的每次走1步 ListNode* fast=head; ListNode* slow=head; while(fast->next!=nullptr){ fast=fast->next->next; slow=slow->next; //此時有兩個中間節點,slow剛好指向第二個 if(fast==nullptr){ return slow; } } //只有一箇中間節點 return slow; } };
判斷連結串列是否包含環
判斷連結串列是否包含環屬於經典問題了,解決方案也是用快慢指標:
每當慢指標 slow
前進一步,快指標 fast
就前進兩步。
如果 fast
最終遇到空指標,說明連結串列中沒有環;如果 fast
最終和 slow
相遇,那肯定是 fast
超過了 slow
一圈,說明連結串列中含有環。
當然,這個問題還有進階版:如果連結串列中含有環,如何計算這個環的起點?
可以看到,當快慢指標相遇時,讓其中任一個指標指向頭節點,然後讓它倆以相同速度前進,再次相遇時所在的節點位置就是環開始的位置。
為什麼要這樣呢?這裡簡單說一下其中的原理。
我們假設快慢指標相遇時,慢指標 slow
走了 k
步,那麼快指標 fast
一定走了 2k
步:
fast
一定比 slow
多走了 k
步,這多走的 k
步其實就是 fast
指標在環裡轉圈圈,所以 k
的值就是環長度的「整數倍」。
假設相遇點距環的起點的距離為 m
,那麼結合上圖的 slow
指標,環的起點距頭結點 head
的距離為 k - m
,也就是說如果從 head
前進 k - m
步就能到達環起點。
巧的是,如果從相遇點繼續前進 k - m
步,也恰好到達環起點。因為結合上圖的 fast
指標,從相遇點開始走k步可以轉回到相遇點,那走 k - m
步肯定就走到環起點了:
所以,只要我們把快慢指標中的任一個重新指向 head
,然後兩個指標同速前進,k - m
步後一定會相遇,相遇之處就是環的起點了。
兩個連結串列是否相交
力扣第 160 題「 相交連結串列」
我的程式碼:
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* a=headA;
ListNode* b=headB;
while(a!=b){
a==nullptr?(a=headB):(a=a->next);
b==nullptr?(b=headA):(b=b->next);
}
return a;
}
};
思路:
本題書中思路比較清晰,無需總結
給你輸入兩個連結串列的頭結點 headA
和 headB
,這兩個連結串列可能存在相交。
如果相交,你的演算法應該返回相交的那個節點;如果沒相交,則返回 null。
比如題目給我們舉的例子,如果輸入的兩個連結串列如下圖:
那麼我們的演算法應該返回 c1
這個節點。
這個題直接的想法可能是用 HashSet
記錄一個連結串列的所有節點,然後和另一條連結串列對比,但這就需要額外的空間。
如果不用額外的空間,只使用兩個指標,你如何做呢?
難點在於,由於兩條連結串列的長度可能不同,兩條連結串列之間的節點無法對應:
如果用兩個指標 p1
和 p2
分別在兩條連結串列上前進,並不能同時走到公共節點,也就無法得到相交節點 c1
。
解決這個問題的關鍵是,通過某些方式,讓 p1
和 p2
能夠同時到達相交節點 c1
。
所以,我們可以讓 p1
遍歷完連結串列 A
之後開始遍歷連結串列 B
,讓 p2
遍歷完連結串列 B
之後開始遍歷連結串列 A
,這樣相當於「邏輯上」兩條連結串列接在了一起。
如果這樣進行拼接,就可以讓 p1
和 p2
同時進入公共部分,也就是同時到達相交節點 c1
:
那你可能會問,如果說兩個連結串列沒有相交點,是否能夠正確的返回 null 呢?
這個邏輯可以覆蓋這種情況的,相當於 c1
節點是 null 空指標嘛,可以正確返回 null。
按照這個思路,可以寫出如下程式碼:
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// p1 指向 A 連結串列頭結點,p2 指向 B 連結串列頭結點
ListNode p1 = headA, p2 = headB;
while (p1 != p2) {
// p1 走一步,如果走到 A 連結串列末尾,轉到 B 連結串列
if (p1 == null) p1 = headB;
else p1 = p1.next;
// p2 走一步,如果走到 B 連結串列末尾,轉到 A 連結串列
if (p2 == null) p2 = headA;
else p2 = p2.next;
}
return p1;
}
這樣,這道題就解決了,空間複雜度為 O(1)
,時間複雜度為 O(N)
。