前言
前面講過線性表中順序表和鏈表的實現
和性質。但是在數據結構與算法中,雙向鏈表無論在考察還是運用中都佔有很大的比例,筆者旨在通過本文與讀者一起學習分享雙鏈表相關知識。
<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;
}
增加
空表插入:
- 對於空鏈表來說。增加第一個元素可以特殊考慮。因爲在鏈表爲空的時候
head
和tail
均爲null。但head和tail又需要實實在在指向鏈表中的真實數據(帶頭指針就不需要考慮)。所以這時候就新建一個node
讓head、tail等於它。
node<T> teamNode = new node(data);
if (isEmpty()) {
head = teamNode;
tail = teamNode;
}
頭插入:
對於頭插入來說。步驟很簡單,只需考慮head節點的變化。
- 新建插入節點node
- head前驅指向node
- node後驅指向head
- head指向node。(這時候head只是表示第二個節點,而head需要表示第一個節點故
重新賦值
)
尾插入:
對於尾插入來說。只需考慮尾節點tail節點的變化。
- 新建插入節點node
- node前驅指向tail
- tail後驅指向node
- tail指向node。(這時候tail只是表示倒數第二個節點,而tail需要表示最後節點故
重新賦值
等於node即可)
編號插入:
對於編號插入來說。要考慮查找和插入兩部,而插入既和head無關也和tail無關。
- 新建插入節點node
- 找到欲插入node的前一個節點
pre
。和後一個節點after
- node後驅指向after,after前驅指向node(次時node和後面節點的關聯已經完成,但是和前面處理分離狀態)
- pre後驅指向node。node前驅指向pre(此時完畢)
整個流程的動態圖爲:
刪除
單節點刪除:
無論頭刪還是尾刪,遇到單節點刪除的需要將鏈表從新初始化!
if (length == 1)// 只有一個元素
{
head = null;
tail = head;
length--;
}
頭刪除:
頭刪除需要注意的就是刪除不爲空時候頭刪除只和head節點有關
大致分爲:
- head節點的後驅節點的前驅節點改爲null。(head後面本指向head但是要刪除第一個先讓後面那個和head
斷絕關係
) - head節點指向head.next.(這樣
head
就指向我們需要的第一個節點了。如果有需要處理內存的語言就可以把第一個被孤立的節點刪除了)
<font color="blue">尾刪除</font>:
尾刪除需要注意的就是刪除不爲空時候尾刪除只和tail節點有關。記得在普通鏈表中,我們刪除尾節點需要找到尾節點的前驅節點。需要遍歷整個表。而雙向鏈表可以直接從尾節點遍歷到前面。
刪除的時tail所在位置的點。也就是tail所在節點要斷絕和雙鏈表的關係。
-
tail.pre.next=null
尾節點的前一個節點(pre)的後驅節點等於null -
tail=tail.pre
尾節點指向它的前驅節點,此時尾節點由於步驟1
next已經爲null。完成刪除
普通刪除:
普通刪除需要重點掌握,因爲前兩個刪除都是普通刪除的一個特例而已。(普通刪除要確保不是頭刪除和尾刪除)
- 找打將刪除節點的前驅節點team(
team.next
是要刪除的節點) -
team.next.next.pre=team
.(欲被刪除節點的後一個節點的前驅指向team,雙向鏈表需要處理pre和next。這步處理了pre)
-
team.next=team.next.next;
此時team.next也跳過被刪除節點。
- 完成刪除
整個流程的動態圖爲:
代碼與測試
代碼:
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
- 歡迎交友!