1. 程式人生 > >資料結構與演算法—一文多圖搞懂雙鏈表

資料結構與演算法—一文多圖搞懂雙鏈表

前言

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

 

雙鏈表介紹


與單鏈表區別

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

  • 對於一個節點,有儲存資料的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又需要實實在在指向連結串列中的真實資料(帶頭指標就不需要考慮)。所以這時候就新建一個node讓head、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就指向我們需要的第一個節點了。如果有需要處理記憶體的語言就可以把第一個被孤立的節點刪除了)

 

 

尾刪除:

尾刪除需要注意的就是刪除不為空時候尾刪除只和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
  • 關注後回覆 資料結構 即可獲得精心準備資料一份!