讓鏈表題目再也不復雜

前言

咱們接着上部分的二分查找,再繼續鏈表相關的題目java

換一個角度來理解鏈表

我相信你們對鏈表的數據結構已經很熟悉了。什麼單鏈表,循環鏈表,雙向鏈表,雙向循環鏈表。程序員

咱們這裏以java的層面來理解指針或引用的含義:算法

實際上,鏈表其實並不複雜,複雜的是咱們很容易將它和指針混淆在一塊兒。就會讓人產生疑惑。因此想要寫好鏈表的代碼。就得拋開舊識,從新理解指針。數據結構

咱們知道,有些語言有「指針」的概念,好比 C 語言;有些語言沒有指針,取而代之的是「引用」,好比 Java、Python。不論是「指針」仍是「引用」,實際上,它們的意思都是同樣的,都是存儲所指對象的內存地址。函數

因此做爲java程序員,咱們要緊緊記住,什麼是引用:this

將某個變量賦值給引用,實際上就是將這個變量的地址賦值給引用,或者反過來講,引用中存儲了這個變量的內存地址,指向了這個變量,經過引用就能找到這個變量。指針

以下面這段代碼,就是一個鏈表結構的對象。當咱們在調用引用移來移去的時候。你要緊緊記住,本身操做的是這個對象。其餘引用該對象的都會收到影響。code

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

神奇不,鏈表在此時就成功的反轉了。因此掌握遞歸解法也是很重要的。不少很複雜的指針解法,換個思路說不定也會很是的簡單。

總結

寫鏈表代碼是最考驗邏輯思惟能力的。寫的時候必定要考慮全面。最好將我剛纔說的容易錯的狀況都在腦子裏過一遍。

對於部分題解,換個思路使用遞歸。可能會簡單不少。

多看,多練,多思考

相關文章
相關標籤/搜索