1. 程式人生 > 其它 >雙指標技巧秒殺七道連結串列題目(完)

雙指標技巧秒殺七道連結串列題目(完)

雙指標技巧秒殺七道連結串列題目(2)

單鏈表的倒數第 k 個節點

從前往後尋找單鏈表的第 k 個節點很簡單,一個 for 迴圈遍歷過去就找到了,但是如何尋找從後往前數的第 k 個節點呢?

這個解法就比較巧妙了,假設 k = 2,思路如下:

首先,我們先讓一個指標 p1 指向連結串列的頭節點 head,然後走 k 步:

現在的 p1,只要再走 n - k 步,就能走到連結串列末尾的空指標了對吧?

趁這個時候,再用一個指標 p2 指向連結串列頭節點 head

接下來就很顯然了,讓 p1p2 同時向前走,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 個節點,也就是中間節點。

我們讓兩個指標 slowfast 分別指向連結串列頭結點 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;
    }
};

思路:

​ 本題書中思路比較清晰,無需總結

給你輸入兩個連結串列的頭結點 headAheadB,這兩個連結串列可能存在相交。

如果相交,你的演算法應該返回相交的那個節點;如果沒相交,則返回 null。

比如題目給我們舉的例子,如果輸入的兩個連結串列如下圖:

那麼我們的演算法應該返回 c1 這個節點。

這個題直接的想法可能是用 HashSet 記錄一個連結串列的所有節點,然後和另一條連結串列對比,但這就需要額外的空間。

如果不用額外的空間,只使用兩個指標,你如何做呢?

難點在於,由於兩條連結串列的長度可能不同,兩條連結串列之間的節點無法對應:

如果用兩個指標 p1p2 分別在兩條連結串列上前進,並不能同時走到公共節點,也就無法得到相交節點 c1

解決這個問題的關鍵是,通過某些方式,讓 p1p2 能夠同時到達相交節點 c1

所以,我們可以讓 p1 遍歷完連結串列 A 之後開始遍歷連結串列 B,讓 p2 遍歷完連結串列 B 之後開始遍歷連結串列 A,這樣相當於「邏輯上」兩條連結串列接在了一起。

如果這樣進行拼接,就可以讓 p1p2 同時進入公共部分,也就是同時到達相交節點 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)