數據結構與算法—一文多圖搞懂雙鏈表

前言

前面講過線性表中順序表和鏈表實現和性質。可是在數據結構與算法中,雙向鏈表不管在考察仍是運用中都佔有很大的比例,筆者旨在經過本文與讀者一塊兒學習分享雙鏈表相關知識。html

圖片來源百度

 

雙鏈表介紹


與單鏈表區別

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

  • 對於一個節點,有儲存數據的data。和next後驅節點(指針)。也就是這個單鏈表想要一些遍歷的操做都得經過前節點—>後節點在這裏插入圖片描述

對於雙鏈表:node

  • 對於一個節點,有些和單鏈表同樣有存儲數據的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又須要實實在在指向鏈表中的真實數據(帶頭指針就不須要考慮)。因此這時候就新建一個node讓head、tail等於它。
node<T> teamNode = new node(data); if (isEmpty()) { head = teamNode; tail = teamNode; } 

頭插入

對於頭插入來講。步驟很簡單,只需考慮head節點的變化。ide

  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就指向咱們須要的第一個節點了。若是有須要處理內存的語言就能夠把第一個被孤立的節點刪除了)

 

在這裏插入圖片描述

 

尾刪除

尾刪除須要注意的就是刪除不爲空時候尾刪除只和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)在這裏插入圖片描述
  3. team.next=team.next.next;此時team.next也跳過被刪除節點。在這裏插入圖片描述
  4. 完成刪除在這裏插入圖片描述整個流程的動態圖爲:在這裏插入圖片描述

代碼與測試


代碼:

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
  • 關注回覆 數據結構 便可得到精心準備資料一份!

 

相關文章
相關標籤/搜索