一文多圖搞懂雙鏈表

前言

前面講過線性表中順序表和鏈表實現和性質。但是在數據結構與算法中,雙向鏈表無論在考察還是運用中都佔有很大的比例,筆者旨在通過本文與讀者一起學習分享雙鏈表相關知識。
圖片來源百度

<font color="green">雙鏈表介紹</font>


與單鏈表區別

邏輯上沒有區別。他們均是完成線性表的內容。主要的區別是結構上的構造有所區別。
對於單鏈表:

  • 對於一個節點,有儲存數據的data。和next後驅節點(指針)。也就是這個單鏈表想要一些遍歷的操作都得通過前節點—>後節點

在這裏插入圖片描述

對於雙鏈表:

  • 對於一個節點,有些和單鏈表一樣有存儲數據的data,指向後方的next(指針)。它擁有單鏈表的所有操作和內容。但是他還有一個前驅節點pre(指針)。

在這裏插入圖片描述

結構的設計

  • 對於雙鏈表的結構,上圖也很清楚的。以前設計的單鏈表是帶頭節點的。帶頭節點可以方面首位的插入和刪除。而這次我們抱着學習的態度搞清鏈表故該雙鏈表是不帶頭節點的.
  • 同時,以前的單鏈表是不帶尾節點的,這次我們帶上尾節點tail。這樣我們就接觸了幾乎所有類型啦!遇到啥也不怕了。

所以我們構造的這個雙鏈表的的性質:

  • 不帶頭節點、帶尾指針(tail)、雙向鏈表。

對於node節點:

class node<T> {
    T data;
    node<T> pre;
    node<T> next;

    public node() {
    }

    public node(T data) {
        this.data = data;
    }
}

對於鏈表:

public class doubleList<T> {
    private node<T> head;// 頭節點
    private node<T> tail;// 尾節點
    private int length;
    //各種方法    
}

具體方法的解析

  • 其實對於一個鏈表主要的操作還是增刪。增閃的話都需要考慮是否帶頭節點。頭插尾插中間插。並且還要考慮其中的一些細節處理。指針的運算。防止鏈表崩掉。因爲這些操作如果不當往往會對鏈表的結構和證悟性帶來致命的打擊。而像查找那些邏輯稍微簡單。也很容易排查錯誤。

初始化

  • 我們知道一個雙鏈表在最初的時候它的數據肯定是爲null的。那麼對於這個不帶頭節點的雙鏈表而言。它的head始終指向第一個真實有效的數據。tail也是如此。那麼在最初沒數據的時候當然要head=null,並且tail=head。(tail和head需要在一個鏈上)。
public doubleList() {
    head = null;
    tail = head;
    length = 0;
    }

增加

空表插入:

  • 對於空鏈表來說。增加第一個元素可以特殊考慮。因爲在鏈表爲空的時候headtail均爲null。但head和tail又需要實實在在指向鏈表中的真實數據(帶頭指針就不需要考慮)。所以這時候就新建一個nodehead、tail等於它
node<T> teamNode = new node(data);
if (isEmpty()) {
    head = teamNode;
    tail = teamNode;    
}

頭插入:

對於頭插入來說。步驟很簡單,只需考慮head節點的變化。
  1. 新建插入節點node
  2. head前驅指向node
  3. node後驅指向head
  4. head指向node。(這時候head只是表示第二個節點,而head需要表示第一個節點故重新賦值)

在這裏插入圖片描述

尾插入:

對於尾插入來說。只需考慮尾節點tail節點的變化。
  1. 新建插入節點node
  2. node前驅指向tail
  3. tail後驅指向node
  4. tail指向node。(這時候tail只是表示倒數第二個節點,而tail需要表示最後節點故重新賦值等於node即可)

在這裏插入圖片描述

編號插入:

對於編號插入來說。要考慮查找和插入兩部,而插入既和head無關也和tail無關。
  1. 新建插入節點node
  2. 找到欲插入node的前一個節點pre。和後一個節點after
  3. node後驅指向after,after前驅指向node(次時node和後面節點的關聯已經完成,但是和前面處理分離狀態)在這裏插入圖片描述
  4. pre後驅指向node。node前驅指向pre(此時完畢)

在這裏插入圖片描述

整個流程的動態圖爲:
在這裏插入圖片描述

刪除

單節點刪除:

無論頭刪還是尾刪,遇到單節點刪除的需要將鏈表從新初始化!
if (length == 1)// 只有一個元素
{
    head = null;
    tail = head;
    length--;
}

頭刪除:

頭刪除需要注意的就是刪除不爲空時候頭刪除只和head節點有關

大致分爲:

  1. head節點的後驅節點前驅節點改爲null。(head後面本指向head但是要刪除第一個先讓後面那個和head斷絕關係)
  2. head節點指向head.next.(這樣head就指向我們需要的第一個節點了。如果有需要處理內存的語言就可以把第一個被孤立的節點刪除了)

在這裏插入圖片描述

<font color="blue">尾刪除</font>:

尾刪除需要注意的就是刪除不爲空時候尾刪除只和tail節點有關。記得在普通鏈表中,我們刪除尾節點需要找到尾節點的前驅節點。需要遍歷整個表。而雙向鏈表可以直接從尾節點遍歷到前面。

刪除的時tail所在位置的點。也就是tail所在節點要斷絕和雙鏈表的關係。

  1. tail.pre.next=null尾節點的前一個節點(pre)的後驅節點等於null
  2. tail=tail.pre 尾節點指向它的前驅節點,此時尾節點由於步驟1next已經爲null。完成刪除

在這裏插入圖片描述

普通刪除:

普通刪除需要重點掌握,因爲前兩個刪除都是普通刪除的一個特例而已。(普通刪除要確保不是頭刪除和尾刪除)
  1. 找打將刪除節點的前驅節點team(team.next是要刪除的節點)
  2. team.next.next.pre=team.(欲被刪除節點的後一個節點的前驅指向team,雙向鏈表需要處理pre和next。這步處理了pre)

在這裏插入圖片描述

  1. team.next=team.next.next;此時team.next也跳過被刪除節點。

在這裏插入圖片描述

  1. 完成刪除

在這裏插入圖片描述
整個流程的動態圖爲:
在這裏插入圖片描述

代碼與測試


代碼:

package LinerList;

/*
 * 不帶頭節點的
 */
public class doubleList<T> {
class node<T> {
    T data;
    node<T> pre;
    node<T> next;

    public node() {
    }

    public node(T data) {
        this.data = data;
    }
}

private node<T> head;// 頭節點
private node<T> tail;// 尾節點
private int length;

public doubleList() {
    head = null;
    tail = head;
    length = 0;
}

boolean isEmpty() {
    return length == 0 ? true : false;
}

void addfirst(T data) {
    node<T> teamNode = new node(data);
    if (isEmpty()) {
        head = teamNode;
        tail = teamNode;
        
    } else {
        teamNode.next = head;
        head = teamNode;
    }
    length++;
}

void add(T data)// 尾節點插入
{
    node<T> teamNode = new node(data);
    if (isEmpty()) {
        head = teamNode;
        tail = teamNode;
    } else {
        tail.next = teamNode;
        teamNode.pre=tail;
        tail = teamNode;
    }
    length++;
}
   int length()
   {
       return length;
   }
T getElum(int index)//爲了簡單統一從頭找
{
    node<T> team=head;
    for(int i=0;i<index;i++)//不帶頭節點  遍歷次數-1
    {
        team=team.next;
    }
    return team.data;
}
void add(int index, T data)// 編號插入
{
    if (index == 0) {
        addfirst(data);
    } else if (index == length) {
        add(data);
    } else {// 重頭戲
        node teampre = head;// 爲插入的前qu
        for (int i = 0; i < index -1; i++)// 無頭節點,index-1位置找到前驅節點
        {
            teampre = teampre.next;
        }

        node<T> team = new node(data);// a c 中插入B 找打a
        team.next = teampre.next;// B.next=c
        teampre.next.pre = team;// c.pre=B
        team.pre = teampre;// 關聯a B
        teampre.next = team;
        length++;
    }
}
void deletefirst()// 頭部刪除
{
    if (length == 1)// 只有一個元素
    {
        head = null;
        tail = head;
        length--;
    } else {
        head = head.next;
        length--;
    }
}
 void deletelast() {
    if(length==1)
    {
        head=null;
        tail=head;
        length--;
    }
    else {
        
        tail.pre.next=null;
        tail=tail.pre;
        length--;
        
    }
}
 void delete(int index)
 {
     if(index==0)deletefirst();
     else if (index==length-1) {
        deletelast();
    }
     else {//刪除 爲了理解統一從頭找那個節點  
        node<T>team=head;
        for(int i=0;i<index-1;i++)
        {
            team=team.next;
        }
        //team 此時爲要刪除的前節點  a  c   插入B  a爲team
        team.next.next.pre=team;//c的前驅變成a
        team.next=team.next.next;//a的後驅變成c
        length--;
    }
 }
   void set(int index,T data)
   {
       node<T>team=head;
    for(int i=0;i<index-1;i++)
    {
        team=team.next;
    }
    team.data=data;
   }
@Override
public String toString() {
    node<T> team = head;
    String vaString = "";
    while (team != null) {
        vaString += team.data + " ";
        team = team.next;
    }
    return vaString;
}
}

測試:

package LinerList;

public class test {
    public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
    System.out.println("線性表測試:");

    doubleList<Integer> list = new doubleList<Integer>();
    list.add(66);
    list.addfirst(55);
    list.add(1, 101);
    list.add(-22);
    list.add(555);
    list.addfirst(9999);
    System.out.println(list.toString() + " lenth " + list.length());// 9999 55 101 66 -22 555
    // System.out.println(list.getElum(0)+" "+list.getElum(2)+" "+list.getElum(4));
    list.deletefirst();
    System.out.println(list.toString() + " lenth " + list.length());// 55 101 66 -22 555 lenth 5
    list.delete(1);
    System.out.println(list.toString() + " length " + list.length());// 55 66 -22 555 length 4
    list.delete(1);

    System.out.println(list.toString() + " length " + list.length());// 55 -22 555 length 3
    list.deletelast();
    System.out.println(list.toString() + " lenth " + list.length());// 55 -22 lenth 2
    list.deletelast();
    System.out.println(list.toString() + " lenth " + list.length());// 55 lenth 1
    list.deletelast();
    System.out.println(list.toString() + " lenth " + list.length());// lenth 0
    System.err.println("歡迎關注公衆號:bigsai");

    }

}

結果圖
在這裏插入圖片描述

總結與感悟

插入、刪除順序問題

  • 很多人其實不清楚插入、刪除正確的順序是什麼。其實這點沒有必然的順序,要根據題意所給的條件完成相同的結果即可!
  • 還有就是你可能會搞不清一堆next.next這些問題。這時候建議你畫個圖。你也可以先建一個節點,用變量名完成操作,可能會更容易一些。比如刪除操作,你找到pre節點(刪除前的節點)。你可以node delete=pre.next,node next=delete.next。這樣你直接操作pre。delete。next三個節點會更簡單。
  • 但是很多題目只給你一個node。你這時候要分析next(pre)。改變順序。因爲只有一個節點,你改變next(pre)很可能導致你遍歷不到那個節點。所以這種情況要好好思考(可以參考筆者的代碼實現)。
  • 至於有些語言需要刪除內存的。別忘記刪除。(java大法好)

其他操作問題:

  • 對於其他操作,相比增刪要容易理解,可以參考代碼理解。
  • 雙向鏈表可以對很多操作進行優化。這裏只是突出實現並沒有寫的太多。比如查找時候可以根據長度判斷這個鏈表從頭查找還是從尾查找

另外,代碼寫的可能不是太好,鏈表也沒考慮線程安全問題。算法效率可能不太優。如果有什麼改進或者漏洞還請大佬指出!

最後(last but not least)

  • 喜歡的感覺可以的煩請大家動動小手關注一下把,關注後回覆: 數據結構 有精心準備的系列。個人公衆號交流:bigsai
  • 歡迎交友!

圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章