【手寫資料結構】雙鏈表最詳細圖解

語言: CN / TW / HK

原創公眾號:bigsai
原創不易,如果有收穫請不要吝嗇你的一鍵三連
文章已收錄在 全網都在關注的資料結構與演算法學習倉庫 歡迎star

前言

前面有很詳細的講過線性表(順序表和連結串列),當時講的連結串列以但連結串列為主,但實際上在實際應用中雙鏈表的應用多一些就比如LinkedList。

圖片來源百度

雙鏈表與單鏈表區別

邏輯上它們均是線性表的鏈式實現,主要的區別是節點結構上的構造有所區別,這個區別從而引起操作的一些差異。
單鏈表:

單鏈表的一個節點,有儲存資料的data,還有後驅節點next(指標)。也就是這個單鏈表想要一些遍歷的操作都得通過前節點—>後節點
image-20210311183254265

雙鏈表:

雙鏈表的一個節點,有儲存資料的data,也有後驅節點next(指標),這和單鏈表是一樣的,但它還有一個前驅節點pre(指標)。
image-20210311202253887

雙鏈表結構的設計

上文講單鏈表的時候,我們當時設計的是一個帶頭結點的連結串列就錯過了不帶頭結點操作方式,這裡雙鏈表咱們就不帶頭結點設計實現。並且上文單鏈表實現的時候是沒有尾指標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,此時連結串列為空,等待節點插入。

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需要表示第一個節點故改變指向)

iShot2021-03-12 11.55.52

尾插:

對於尾插入來說。只需考慮尾節點tail節點的變化。

  1. 新建插入節點node
  2. node前驅指向tail
  3. tail後驅指向node
  4. tail指向node。(這時候tail只是表示倒數第二個節點,而tail需要表示最後節點故指向node)

iShot2021-03-12 11.58.03

按編號插入

對於編號插入來說。要考慮查詢和插入兩部,而插入既和head無關也和tail無關。

1 新建插入節點node

2 找到欲插入node的前一個節點preNode。和後一個節點nextNode

3 node後驅指向nextNode,nextNode前驅指向node(此時node和後面與連結串列已經聯立,但是和前面處理分離狀態)

image-20210312144538812

4 preNode後驅指向node。node前驅指向preNode(此時插入完整操作完畢)

image-20210312145043644

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

刪除

只有單個節點刪除

無論頭刪還是尾刪,遇到單節點刪除的需要將連結串列從新初始化!

if (length == 1)// 只有一個元素
{
   
   
   
	head = null;
	tail = head;
	length--;
}

頭刪除

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

流程大致分為:

1 head節點的後驅節點前指標pre改為null。(head後面節點本指向head但是要刪除第一個先讓後面那個和head斷絕關係)

image-20210312151627033

2 head節點指向head.next(這樣head就指向我們需要的第一個節點了,前面節點就被刪除成功,如果有c++等語言第一個被孤立的節點刪除釋放即可,但Java會自動釋放)

image-20210312151739931

尾刪除

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

尾刪除就是刪除雙向連結串列中的最後一個節點,也就是尾指標所指向的那個節點,思想和頭刪除的思想一致,具體步驟為:

  1. tail.pre.next=null尾節點的前一個節點(pre)的後驅節點等於null
  2. tail=tail.pre尾節點指向它的前驅節點,此時尾節點由於步驟1next已經為null。完成刪除
    在這裡插入圖片描述

普通刪除

普通刪除需要重點掌握,普通刪除要妥善處理好待刪除節點的前後關係,具體流程如下:

1:找打待刪除節點node的前驅節點prenode(prenode.next是要刪除的節點)

2 : prenode.next.next.pre=prenode.(將待刪除node的後驅節點aftnode的pre指標指向prenode,等價於aftnode.pre=prenode

image-20210312153817464

3: prenode.next=prenode.next.next;此時node被跳過成功刪除。

image-20210312184319136

完成刪除整個流程的動態圖為:
在這裡插入圖片描述

實現與測試

通過上面的思路簡單的實現一下雙鏈表,當然有些地方命名不太規範,實現效率有待提升,主要目的還是帶著大家理解。

程式碼:



/*
 * 不帶頭節點的
 */
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;// 為插入的前驅
            // 無頭節點,index-1位置找到前驅節點
            for (int i = 0; i < index -1; i++)
            {
   
   
   
                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;
    }
}

測試:

image-20210315170716735

結語

在插入刪除的步驟,很多人可能因為繁瑣的過程而弄不明白,但實際上這個操作的寫法可能是多樣的,但本質操作都是一致的,所以看到其他不同版本有差距也是正常的。

還有很多人可能對一堆next.next搞不清楚,那我教你一個技巧,如果在等號右側,那麼它表示一個節點,如果在等號左側,那麼除了最後一個.next其他的表示節點。例如node.next.next.next可以看成(node.next.next).next。

在做資料結構與演算法連結串列相關題的時候,不同題可能給不同節點去完成插入、刪除操作。這種情況操作時候要謹慎先後順序防止破壞連結串列結構。

程式碼操作可能有些優化空間,還請各位大佬指正!記得一鍵三連支援一下!

在這裡插入圖片描述

本文同步分享在 部落格“Big sai”(CSDN)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。