1. 程式人生 > 實用技巧 >讓連結串列題目不再複雜

讓連結串列題目不再複雜

前言

我們接著上部分的二分查詢,再繼續連結串列相關的題目

換一個角度來理解連結串列

我相信大家對連結串列的資料結構已經很熟悉了。什麼單鏈表,迴圈連結串列,雙向連結串列,雙向迴圈連結串列。

我們這裡以java的層面來理解指標或引用的含義:

實際上,連結串列其實並不複雜,複雜的是我們很容易將它和指標混淆在一起。就會讓人產生疑惑。所以想要寫好連結串列的程式碼。就得拋開舊識,重新理解指標。

我們知道,有些語言有“指標”的概念,比如 C 語言;有些語言沒有指標,取而代之的是“引用”,比如 Java、Python。不管是“指標”還是“引用”,實際上,它們的意思都是一樣的,都是儲存所指物件的記憶體地址。

所以作為java程式設計師,我們要牢牢記住,什麼是引用:

將某個變數賦值給引用,實際上就是將這個變數的地址賦值給引用,或者反過來說,引用中儲存了這個變數的記憶體地址,指向了這個變數,通過引用就能找到這個變數。

如下面這段程式碼,就是一個連結串列結構的物件。當我們在呼叫引用移來移去的時候。你要牢牢記住,自己操作的是這個物件。其他引用該物件的都會收到影響。

public class ListNode {
      int val;
      ListNode next;
      ListNode() {}
      ListNode(int val) { this.val = val; }
      ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 }

解題技巧

警惕指標丟失

初學者寫連結串列的時候,最容易犯的錯誤就是,常常把指標給搞丟了。

我們以向連結串列(a->b->c-null)插入節點來例項:


a.next = x;  
x.next = a.next;  

我一開始寫連結串列程式碼的時候也經常犯這種錯誤。值得注意的是,當我們呼叫第一行程式碼的時候,a的next節點已經指向了x,此時記憶體中存在兩個連結串列

a->x->null
b->c->null

當呼叫第二行程式碼的時候,x的next節點又指向了自己。

所以,我們插入結點時,一定要注意操作的順序,要先將結點 x 的 next 引用指向結點 b,再把結點 a 的 next 引用指向結點 x,這樣才不會丟失指標。

利用哨兵簡化實現難度

我給出一段程式碼,就是連結串列的插入操作

newNode.next = p.next;
p.next = newNode;

但是,如果我們要插入連結串列中的第一個結點,前面的程式碼就不 work 了。我們需要對於這種情況特殊處理。寫成程式碼是這樣子的:

if (head == null) {
    head = newNode;
}

針對連結串列的插入、刪除操作,需要對插入第一個結點和刪除最後一個結點的情況進行特殊處理。這樣程式碼實現起來就會很繁瑣,不簡潔,而且也容易因為考慮不全而出錯。如何來解決這個問題呢?

“哨兵”節點不儲存資料,無論連結串列是否為空,head指標都會指向它,作為連結串列的頭結點始終存在。這樣,插入第一個節點和插入其他節點,刪除最後一個節點和刪除其他節點都可以統一為相同的程式碼實現邏輯了。

重點留意邊界條件處理

我每次寫連結串列相關問題的時候,都會優先判斷幾個條件,看我的程式碼是否能正常work

  1. 如果連結串列為空時,程式碼是否能正常工作?
  2. 如果連結串列只包含一個結點時,程式碼是否能正常工作?
  3. 如果連結串列只包含兩個結點時,程式碼是否能正常工作?
  4. 程式碼邏輯在處理頭結點和尾結點的時候,是否能正常工作?

當你寫完連結串列程式碼之後,除了看下你寫的程式碼在正常的情況下能否工作,還要看下在上面我列舉的幾個邊界條件下,程式碼仍然能否正確工作。如果這些邊界條件下都沒有問題,那基本上可以認為沒有問題了。

用遞迴來解決問題

有些時候,我們可以用遞迴來解決相關的題目可能效果會更好。當然後面會有一篇專門來講解遞迴相關的題目。

我們這裡用一道很基礎的題目來示例。連結串列反轉

public ListNode reverseList(ListNode head) {
        if(head == null || head.next == null){
            return head;
        }       

        ListNode lastNode = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return lastNode;
    }

看起來是不是感覺不知所云,完全不能理解這樣為什麼能夠反轉連結串列?我們下面來詳細解釋一下這段程式碼。

對於遞迴演算法,最重要的就是明確遞迴函式的定義。具體來說,我們的 reverseList 函式定義是這樣的:

輸入一個節點 head,將「以 head 為起點」的連結串列反轉,並返回反轉之後的頭結點。

明白了函式的定義,在來看這個問題。比如說我們想反轉這個連結串列:

1->2->3->4 (head指向1)

那麼輸入 reverse(head) 後,會在這裡進行遞迴():

ListNode last = reverse(head.next);

不要跳進遞迴(你的腦袋能壓幾個棧呀?),而是要根據剛才的函式定義,來弄清楚這段代<碼產<生麼結果:

1->2<-3<-4 (head指向1,lastNode指向4)

然後執行:

    head.next.next = head;
    head.next = null;

此時連結串列將變成:

1<-2<-3<-4

神奇不,連結串列在此時就成功的反轉了。所以掌握遞迴解法也是很重要的。很多很複雜的指標解法,換個思路說不定也會非常的簡單。

總結

寫連結串列程式碼是最考驗邏輯思維能力的。寫的時候一定要考慮全面。最好將我剛才說的容易錯的情況都在腦子裡過一遍。

對於部分題解,換個思路使用遞迴。可能會簡單很多。

多看,多練,多思考