首先,明白什麼是雙向鏈表。所謂雙向鏈表是如果希望找直接前驅結點和直接後繼結點的時間複雜度都是 O(1),那麼,需要在結點中設兩個參考網域,一個儲存直接前驅結點的地址,叫 prev,一個直接後繼結點的地址,叫 next,這樣的鏈表就是雙向鏈表(Doubly Linked List)。雙向鏈表的結點結構。
雙向鏈表結點的定義與單鏈表的結點的定義很相似, ,只是雙向鏈表多了一個欄位 prev。其實,雙向鏈表更像是一根鏈條一樣,你連我,我連你,不清楚,請看圖。
雙向鏈表結點類的實現如下所示
//一個鏈條的類
public class DbNode<T>
{
//當前的資料所在
private T data; //資料域記錄的資料的
private DbNode<T> prev; //前驅參考網域 前驅 引用位置
private DbNode<T> next; //後繼參考網域 後來鏈條的位置
//構造器 這是不是初始化
public DbNode(T val, DbNode<T> p)
{
data = val;
next = p;
}
//構造器 這是不是初始化
public DbNode(DbNode<T> p)
{
next = p;
}
//構造器 吧這個鏈子相應值 傳遞給他
public DbNode(T val)
{
data = val;
next = null;
}
//構造器 構造一個空的鏈子
public DbNode()
{
data = default(T);
next = null;
}
//資料域屬性
public T Data
{
get
{
return data;
}
set
{
data = value;
}
}
//前驅參考網域屬性
public DbNode<T> Prev
{
get
{
return prev;
}
set
{
prev = value;
}
}
//後繼參考網域屬性
public DbNode<T> Next
{
get
{
return next;
}
set
{
next = value;
}
}
}
說了這麼多雙向鏈表接點的類的屬性,我們要看一看他的相關的操作。這裡只做一些畫龍點睛地方的描述
插入操作:設 p是指向雙向鏈表中的某一結點,即 p儲存的是該結點的地址,現要將一個結點 s 插入到結點 p 的後面,插入的原始碼如下所示:操作如下:
➀ p.Next.Prev = s;
➁ s.Prev = p;
➂ s.Next = p.Next;
➃ p.Next = s;
插入過程(以 p 的直接後繼結點存在為例) 。
注意:參考網域值的操作的順序不是唯一的,但也不是任意的,操作➂必須放到操作➃的前面完成,否則 p 直接後繼結點的就找不到了。這一點需要讀者把每個操作的含義搞清楚。此演算法時間操作消耗在尋找上,其時間的複雜度是O(n).
下面,看他的刪除操作,以在結點之後刪除為例來說明在雙向鏈表中刪除結點的情況。 設 p是指向雙向鏈表中的某一結點,即 p儲存的是該結點的地址,現要將一個結點 s插入到結點 p的後面 。虛擬碼如下:操作如下:
➀ p.Next = P.Next.Next;
➁ p.Next.Prev = p.Prev;
刪除過程(以 p的直接後繼結點存在為例)
相應的演算法的時間複雜度也是消耗到結點的尋找上,其複雜度應該是O(n)
尋找操作與單鏈表的極其的類似,也是從頭開始遍曆。相應虛擬碼:
current.next=p.next.next
current.prev=p.next.prev;
相應的虛擬碼如所示:
該演算法的時間複雜度,是一個個的遍曆的過程中,顧時間複雜度是O(n)
擷取當前的雙向鏈表長度與 尋找類似,不做過多的贅述,這裡,我們把雙向鏈表基本概念和操作基本介紹完了,下面介紹一個重要的鏈表——環形鏈表。
首先,還是老樣子,看看環形鏈表的定義。有些應用不需要鏈表中有明顯的頭尾結點。在這種情況下,可能需要方便地從最後一個結點訪問到第一個結點。此時,最後一個結點的參考網域不是Null 參考,而是儲存的第一個結點的地址(如果該鏈錶帶結點,則儲存的是頭結點的地址) ,也就是頭引用的值。我們把這樣的鏈表結構稱之為環形鏈表。他就像小朋友手拉手做遊戲。。
用鏈表:
這裡基本添加,刪除,操作的操作與單鏈表簡直是一模一樣,這裡就沒有必要寫這些東西。我們主要看他們一些簡單應用。
應用舉例一 已知單鏈表 H,寫一演算法將其倒置,即實現的操作,其中(a)為倒置前,(b)為倒置後。
演算法思路:由於單鏈表的儲存空間不是連續的,所以,它的倒置不能像順表那樣,把第 i 個結點與第 n-i 個結點交換(i 的取值範圍是 1 到 n/2,n 為單鏈表的長度) 。其解決辦法是依次取單鏈表中的每個結點插入到新鏈表中去。並且,為了節省記憶體資源,把原鏈表的頭結點作為新鏈表的頭結點。儲存整數的單鏈表的倒置的演算法實現如下:
public void ReversLinkList(LinkList<int> H)
{
Node<int> p = H.Next;
Node<int> q = new Node<int>();
H.Next = null;
while (p != null)
{
q = p;
p = p.Next;
q.Next = H.Next;
H.Next = q;
}
}
該演算法要對鏈表中的結點順序掃描一遍才完成了倒置,所以時間複雜度為O(n),但比同樣長度的順序表多花一倍的時間,因為順序表只需要掃描一半的資料元素。這個是不是你已經頭腦糊了嗎?如果糊了把,請看我的圖例的解釋。
舉例2,約瑟夫環問題,題目如下:
已知n個人(以編號1,2,3...n分別表示)圍坐在一張圓桌周圍。從編號為k的人開始報數,數到m的那個人出列;他的下一個人又從1開始報數,數到m的那個人又出列;依此規律重複下去,直到圓桌周圍的人全部出列。求最後出列的人相應的編號。
void JOSEPHUS(int n,int k,int m) //n為總人數,k為第一個開始報數的人,m為出列者喊到的數
{
/* p為當前結點 r為輔助結點,指向p的前驅結點 list為前端節點*/
LinkList p,r,list; /*建立迴圈鏈表*/
for(int i=0;i<n;i++)
{
p=(LinkList)LNode;
p.data=i;
if(list==NULL)
list=p;
else
r.link=p;
r=p;
}
p.link=list; /*使鏈表迴圈起來*/
p=list; /*使p指向前端節點*/
/*把當前指標移動到第一個報數的人*/
for(i=0;i<k;i++)
{
r=p;
p=p.link;
}
/*迴圈地刪除隊列結點*/
while(p.link!=p)
{
for(i=0;i<m-1;i++)
{
r=p;
p=p.link;
}
r.link=p.link;
console.writeline("被刪除的元素:{0} ",p.data);
free(p);
p=r.node.;
}
console.writeLine("\n最後被刪除的元素是:{0}",P.data);
具體的演算法,:
這個演算法的時間的複雜度是O(n2)
}
還和大家分享的一個例子,就是我做做一個類似與網易郵箱的產品時候,幾千萬甚至數以億級的大數量登入的時候,發現使用者登入的時候真他媽的慢,你猜我開始是怎麼做的,就是直接查資料庫,這當然是不行的。這怎麼辦了, 最後,我在一個高人的指教下,發現登入的時候速度飛快,怎麼搞的。我把所有的資料庫的資料讀入到記憶體中,然後把資料用鏈表把他們串起來,到我查詢某個使用者時候,只比較使用者的 位元組數。
這就是我眼中的鏈表結構。