上節說過這節會講雙向鏈表,環形鏈表和應用舉例,我們開始吧!!!!
首先,明白什么是雙向鏈表。所謂雙向鏈表是如果希望找直接前驅結點和直接后繼結點的時間復雜度都是 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)
獲取當前的雙向鏈表長度與 查找類似,不做過多的贅述,這里,我們把雙向鏈表基本概念和操作基本介紹完了,下面介紹一個重要的鏈表——環形鏈表。
首先,還是老樣子,看看環形鏈表的定義。有些應用不需要鏈表中有明顯的頭尾結點。在這種情況下,可能需要方便地從最后一個結點訪問到第一個結點。此時,最后一個結點的引用域不是空引用,而是保存的第一個結點的地址(如果該鏈表帶結點,則保存的是頭結點的地址) ,也就是頭引用的值。我們把這樣的鏈表結構稱之為環形鏈表。他就像小朋友手拉手做游戲。如圖所示。
用鏈表如圖所示:
這里基本添加,刪除,操作的操作與單鏈表簡直是一模一樣,這里就沒有必要寫這些東西。我們主要看他們一些簡單應用。
應用舉例一 已知單鏈表 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);
具體的算法,如圖所示:
}
還和大家分享的一個例子,就是我做做一個類似與網易郵箱的產品時候,幾千萬甚至數以億級的大數量登錄的時候,發現用戶登錄的時候真他媽的慢,你猜我開始是怎么做的,就是直接查數據庫,這當然是不行的。這怎么辦了, 最后,我在一個高人的指教下,發現登錄的時候速度飛快,怎么搞的。我把所有的數據庫的數據讀入到內存中,然后把數據用鏈表把他們串起來,到我查詢某個用戶時候,只比較用戶的 字節數。
這就是我眼中的鏈表結構。