刷了LeetCode的鏈表專題,我發現了一個祕密!

引言

鏈表是以節點(node)存儲的鏈式存儲結構,一個node包含一個data域(存放數據)和一個next域(存放下一個node的指針),鏈表的各個節點不必定是連續的,它能夠分爲帶頭結點不帶頭結點。頭結點僅包含next域。java

image-20201103222213252

若是不熟悉鏈表的同窗,建議先看看個人一篇文章。在這篇文章中,主要講解使用鏈表的小技巧,如何使用這些技巧來解題,深刻解析了LeetCode中具備表明性的鏈表題目,相信我,看了這篇文章,你不再用擔憂關於鏈表的題目了。node

一、鏈表的幾個概念講解

事實上,鏈表的結構比較簡單,阻礙咱們理解鏈表的經常是由於鏈表的指針邊界問題等,這有時會讓咱們很煩躁,不要慌,咱們下面一一對這下概念解析,相信你看了會有收穫。面試

1.1鏈表中的的指針是什麼

咱們學習C語言時,學過指針,它描述的是指向一個內存地址,在Java語言中,是不存在指針的,可是咱們能夠把它理解爲引用算法

當咱們將某個變量(對象)賦值給指針(引用),實際上就是將這個變量(對象)的地址賦值給指針(引用)。數組

p—>next = q; //表示p節點的後繼指針存儲了q節點的內存地址。
p—>next = p—>next—>next; //表示p節點的後繼指針存儲了p節點的下下個節點的內存地址。
複製代碼

1.1指針指向哪兒

咱們寫鏈表代碼時,使用的指針的指來指去,很快就把咱們搞糊塗了,在這種狀況下很容易發生指針丟失內存泄漏。咱們先普及下這兩個概念:markdown

指針丟失:本身定義的指針不知道指到哪裏了,沒有明確的指向。數據結構

內存泄漏:鏈表中的節點沒有確切的指針判斷,運行時會拋出空指針異常。函數

咱們以插入節點刪除結點來分析指針丟失和內存泄漏的具體狀況oop

  • 插入節點

在節點a和節點b之間插入節點x,b是a的下一節點,p指針指向節點a,學習

p—>next = x;
x—>next = p—>next; 
複製代碼

這樣的代碼會形成指針丟失和內存泄漏,由於這會致使x節點的後繼指針指向了本身自己。

正確代碼應該爲:

x—>next = p—>next;
p—>next = x;
複製代碼

image-20201103224355467

  • 刪除節點

一樣的,在節點a和節點c之間刪除節點b,b是a的下一節點,p指針指向節點a,正確的代碼應該爲:

p—>next = p—>next—>next;
複製代碼

image-20201103234222288

在刪除節點,考慮到刪除的節點多是鏈表中的第一個節點,咱們一般在鏈表頭部加入哨兵(頭結點),這樣可使得刪除鏈表的代碼是一致的,不用再額外考慮是不是第一個節點的狀況。

在鏈表加入哨兵的代碼爲

//定義一個哨兵做爲傳入鏈表的頭結點
 ListNode pre =new ListNode(0);
 pre.next=head;
複製代碼

image-20201103233816980

1.3判斷邊界的條件

處理鏈表問題時,要充分考慮鏈表的邊界判斷條件,一般狀況下,咱們常用如下幾種判斷條件:

  • 若是鏈表爲空時,代碼是否能正常工做?

  • 若是鏈表只包含一個結點時,代碼是否能正常工做?

  • 若是鏈表只包含兩個結點時,代碼是否能正常工做?

  • 代碼邏輯在處理頭結點和尾結點的時候,是否能正常工做?

這些判斷條件須要結合本身的實際場景來使用

二、必須掌握的幾類題目

在上面的學習中,咱們對鏈表的一些易錯的概念進行了解析,下面,咱們就真正的代碼實踐,我在LeetCode上刷題時發現,鏈表題目一般分爲如下幾類:

  • 單鏈表的反轉(LeetCode206)
  • 鏈表中環的檢測(LeetCode141)
  • 兩個有序鏈表的合併(LeetCode21)
  • 刪除鏈表(LeetCode18)
  • 刪除鏈表倒數第n個結點(LeetCode19)
  • 求鏈表的中間結點(LeetCode876)

這幾類鏈表題基本涵蓋了大部分知識點,在下面的學習中,咱們將一一攻克它,相信掌握它們以後,在之後筆試/面試中,更能爲所欲爲。

2.1單鏈表反轉(LeetCode206)

思路:從前日後將每一個節點的指針反向,即.next內的地址換成前一個節點的,但爲了防止後面鏈表的丟失,在每次換以前須要先建立個指針指向下一個節點。

class Solution {
    public ListNode reverseList(ListNode head) {

if(head==null||head.next==null){
return head;
}
    ListNode p1=head;
      //用一個新的鏈表
    ListNode p2=null;
    while(p1!=null){
        //每次更換指向以前都須要保存下一個節點
        ListNode temp=p1.next;
        p1.next=p2;
        p2=p1;
        p1=temp;
    }
    return p2;
    }
}
複製代碼

2.2鏈表中環的檢測(LeetCode141)

思路:定義兩個指針,p1和p2,指針p1每次走一步,指針p2每次走兩步,若是鏈表中存在環,則必然會在某個時刻知足p1==p2

public class Solution {
    public boolean hasCycle(ListNode head) {
        if(head==null||head.next==null){
            return false;
        }
        ListNode slow=head;
        ListNode fast=head.next;
       while(fast!=null&&fast.next!=null){
            if(slow==fast){
                return true;
            }
            slow=slow.next;
            fast=fast.next.next;
        }
        return false;
    }
}
複製代碼

NOTE:對於快指針來講,由於一次跳兩步,若是要使用快指針做爲判斷條件,fast和fast.next都須要判斷是否爲空。(不可跨級

2.3兩個有序的鏈表合併(LeetCode21)

思路:能夠新建立一個鏈表用於合併後的結果,合併的條件以下

  • 兩個鏈表都不爲空

定義一個指針,查找合適的節點並放入新建立鏈表的下一位置

  • 有一個鏈表爲空

將不爲空的鏈表放入新建立鏈表的下一位置

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {

        if(l1==null){
            return l2;
        }
        if(l2==null){
            return l1;
        }
        ListNode result=new ListNode(0);
        ListNode temp=result;
        //兩個鏈表都不爲空
        while(l1!=null&&l2!=null){
            if(l1.val<=l2.val){
                temp.next=l1;
                temp=temp.next;
                l1=l1.next;
            }
            else{
                temp.next=l2;
                temp=temp.next;
                l2=l2.next;
            }
        }
 //有一個鏈表爲空
       if(l1==null){
           temp.next=l2;
       }
       else{
           temp.next=l1;
       }

        return result.next;
    }
}
複製代碼

2.4刪除鏈表(LeetCode18)

思路:能夠在鏈表頭加一個哨兵(頭結點),刪除鏈表時先找到刪除鏈表的上一個位置,按照刪除規則刪除便可。

class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        if(head==null){
            return null;
        }
        //定義一個哨兵做爲傳入鏈表的頭結點
        ListNode pre =new ListNode(0);
        pre.next=head;
        
        ListNode temp=pre;
        while(temp!=null){
            if(temp.next.val==val){
                temp.next=temp.next.next;
                break;
            }
            else{
                temp=temp.next;
            }
        }
        return pre.next;
    }
}
複製代碼

2.5刪除鏈表倒數第 n 個結點(LeetCode19)

思路:刪除節點時要利用好哨兵(帶頭結點的鏈表)

  • 遍歷數組的長度count
  • 找到要刪除節點的前一個位置count-n-1
  • 刪除節點
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode pre=new ListNode(0);
        pre.next=head;
        ListNode temp1=pre;
        ListNode temp2=pre;
        int count=0;
        while(temp1!=null){
            temp1=temp1.next;
            count++;
        }

        while(count-n-1>0){
            temp2=temp2.next;
            count--;
        }
        temp2.next=temp2.next.next;
        
        return pre.next;
    }
}
複製代碼

2.6求鏈表的中間結點(LeetCode876)

思路:找出鏈表中結點的個數count,而後count/2找出中間結點,刪除便可。

class Solution {
    public ListNode middleNode(ListNode head) {
        if(head==null) return null;
        ListNode temp=head;
       int count=0;
       while(temp!=null){
        temp=temp.next;
        count++;
       }
       int mid=count/2;
       ListNode result=head;
       while(mid>0){
          result=result.next;
          mid--;
       }
       return result;
    }
}
複製代碼

Note:實踐是檢驗真理的惟一標準,要真正的學好鏈表這個知識點,僅僅學理論是不可靠的,咱們須要多敲代碼多思考多寫多練,針對抽象的題目,能夠舉例畫圖,來輔助的本身的思考。

三、學習鏈表的體會

一、 函數中須要移動鏈表時,最好新建一個指針來移動,以避免更改原始指針位置。

二、 單鏈表有帶頭節點不帶頭結點的鏈表之分,通常作題默認頭結點是有值的

三、 鏈表的內存時不連續的,一個節點佔一塊內存,每塊內存中有一塊位置(next)存放下一節點的地址。

三、 鏈表中找的思想:快慢指針,建立兩個指針,一個快指針:一次走兩步一個慢指針:一次走一步,若相遇則有環,若指向null則無環。

四、 鏈表找倒數第k個節點思想:建立兩個指針,第一個指針查詢鏈表中結點的個數count,而後count-k肯定刪除結點的位置,用第二個指針遍歷鏈表到count-n-1個位置。

五、 反向鏈表思想:從前日後將每一個節點的指針反向,即next內的地址換成前一個節點的,但爲了防止後面鏈表的丟失,在每次換以前須要先建立個指針指向下一個節點

總結

不管學習任何一個知識點,咱們都須要在掌握術(使用方法)的基礎上,學習道(本源),學習數據結構與算法也是同樣,咱們不只要掌握如何使用它,更要掌握爲何要是用它,相比其它的方法,它有什麼優勢,難道是時間複雜度低空間複雜度小,仍是它的數據結構適合這個場景等等...

參考文獻

[1]王爭.數據結構與算法之美

[2]LeetCode中國網站

刷題組合拳推薦

筆者在過去的3個月時間裏整理經常使用的數據結構與算法秒殺劍指offer,公衆號分別回覆數據結構與算法秒殺劍指offer,便可領取兩套電子書,但願可以幫助你們。

我是Simon郎,一個想要天天博學一點點的小青年,關注我,讓咱們一塊兒進階,一塊兒博學。

相關文章
相關標籤/搜索