線性表鏈式存儲結構
看了線性表的順序存儲,你肯定想線性表簡是挺簡單,但是我一開始怎么會知道有多少人排隊?要分配多大的數組?而且插入和刪除一個元素也太麻煩了,所有元素都要前移/后移,效率又低。
那怎么辦呢?
這里就要用到我們的鏈式存儲結構。
這就和我們的鏈條一樣,一環接着一環,只要上一環知道下一環就行了。
你肯定覺得像食堂打飯那樣太麻煩了,每個人都要傻傻地站在那里等,排隊的位置又不夠,人多了排隊的地方都沒有。那有沒有別的方式?
也許我們可以換種方式。每個人都不需要站在那里等,也不需要知道隊伍有多長前面有多少人,只需要知道你的下一個人(也就是后繼)是誰就行了。比如第一個人去了打飯窗口,食堂大媽要他留下電話號碼,隨便去哪都行,到了打飯的時間點就電話通知他。第二個人也過來打飯,食堂大媽也讓第二個人留下手機號,並且將他的手機號告訴第一個人:如果第一個人飯打完了就打電話叫第二個人來打飯。然后再將第三個人的號碼告訴第二個人,由第二個人去通知第三個人,以此類推。這樣每個人都不用排隊了。
第一個人也就是頭節點(頭指針)。
當然,這在現實生活中是不可能的,那樣子大媽會累死,再要碰到誰忘記通知下一個人,整個鏈表就斷了。
初始化
每個人都不用呆在固定的地點排隊,排隊窗口的大小自然也就無所謂了,初始化時只需要知道第一個人的號碼就可以了。當然鏈式結構存儲的方式也和順序不一樣,不需要定義一個數組(因為沒人排隊了),但是需要保存一組數據及它的后繼引用(指針):
public class LinkNode<T>
{
public T Node
{
get; set;
}
public LinkNode<T> NextNode
{
get; set;
}
}
然后定義頭節點:
private LinkNode<T> firstNode;
獲取線性表的長度
想知道有多少人排隊數一下就可以了,但是現在大家都沒排隊,怎么知道總共有多少人在等呢?那只有找到第一個人,再通過第一個人找到第二個,以此類推,直到找到沒有后繼的最后一個為止:
public int Length
{
get
{
var node = firstNode;
int length = 0;
while(node != null)
{
length++;
node = node.NextNode;
}
return length;
}
}
添加元素
每來一個人打飯,就需要將新來那個人的號碼告訴之前的最后一個人:
public void Add(T item)
{
if(firstNode == null)
{
firstNode = new LinkNode<T> { Node = item };
}
else
{
LinkNode<T> node = firstNode;
while(node.NextNode != null)
{
node = node.NextNode;
}
node.NextNode = new LinkNode<T> { Node = item };
}
}
刪除元素
如果你女朋友(程序員竟然也能找到女朋友!)突然臨時約你出去吃飯,你當然覺得食堂這么難吃的飯給程序員吃就可以了,怎么可以給女朋友吃呢?但是作為一名有素質的程序員,你自然會想辦法通知前一個人你不在食堂吃飯了,然后把你下一位的號碼告訴你前一個人。但是你也不知道你前一個人是誰,於是找到食堂大媽,大媽找到第一個人,然后一層層找下去,終於找到你前一個人是誰(想一想這里可以怎樣優化,讓你直接找到前一個人):
public void Delete(int index)
{
if(firstNode == null)
{
throw new Exception("List is empty");
}
if(index < 0)
{
throw new Exception("Location exception");
}
var node = firstNode.NextNode;
var prevNode = firstNode;
int i;
for(i = 1; i < index && node != null; i++)
{
prevNode = node;
node = node.NextNode;
}
if(i == index)
{
if(node.NextNode != null)
{
prevNode.NextNode = node.NextNode;
}
else
{
prevNode.NextNode = null;
}
}
else
{
throw new Exception("Locaion exception");
}
}
在指定位置插入元素
你女朋友約你去吃飯,於是你正打算放棄排隊帶她到外面吃,但是她是個勤儉持家的好女孩,認為外面吃飯太貴,還不如一起在食堂吃,省下來的錢給她買個包。正有此意又不好意思說的你一聽趕緊把女朋友插在你后面。也就是你打完飯不再去通知你的下一位而是通知你女朋友,然后你女朋友再去通知他。是不是覺得鏈表的方式插隊太方便了?神不知鬼不覺就能無痛插隊,而且你女朋友通知他時,他還得說謝謝,這就是鏈式存儲的優勢:
public void Insert(T item, int index)
{
if (firstNode == null)
{
throw new Exception("List is empty");
}
if (index < 0)
{
throw new Exception("Location exception");
}
var node = firstNode.NextNode;
int i;
for (i = 1; i < index && node != null; i++)
{
node = node.NextNode;
}
if (i == index)
{
var nextNode = node.NextNode == null ? null : node.NextNode;
node.NextNode = new LinkNode<T> { Node = item, NextNode = nextNode };
}
else
{
throw new Exception("Locaion exception");
}
}
鏈式存儲完整代碼
/// <summary>
/// 線性表鏈式結構
/// </summary>
public class LinkList<T> : ILinearList<T>
{
private LinkNode<T> firstNode;
public int Length
{
get
{
var node = firstNode;
int length = 0;
while(node != null)
{
length++;
node = node.NextNode;
}
return length;
}
}
public void Add(T item)
{
if(firstNode == null)
{
firstNode = new LinkNode<T> { Node = item };
}
else
{
LinkNode<T> node = firstNode;
while(node.NextNode != null)
{
node = node.NextNode;
}
node.NextNode = new LinkNode<T> { Node = item };
}
}
public void Clear()
{
firstNode = null;
}
public void Delete(int index)
{
if(firstNode == null)
{
throw new Exception("List is empty");
}
if(index < 0)
{
throw new Exception("Location exception");
}
var node = firstNode.NextNode;
var prevNode = firstNode;
int i;
for(i = 1; i < index && node != null; i++)
{
prevNode = node;
node = node.NextNode;
}
if(i == index)
{
if(node.NextNode != null)
{
prevNode.NextNode = node.NextNode;
}
else
{
prevNode.NextNode = null;
}
}
else
{
throw new Exception("Locaion exception");
}
}
public T GetItem(int index)
{
if (firstNode == null)
{
throw new Exception("List is empty");
}
if (index < 0)
{
throw new Exception("Location exception");
}
var node = firstNode.NextNode;
int i;
for (i = 1; i < index && node != null; i++)
{
node = node.NextNode;
}
if (i == index)
{
return node.Node;
}
else
{
throw new Exception("Locaion exception");
}
}
public void Insert(T item, int index)
{
if (firstNode == null)
{
throw new Exception("List is empty");
}
if (index < 0)
{
throw new Exception("Location exception");
}
var node = firstNode.NextNode;
int i;
for (i = 1; i < index && node != null; i++)
{
node = node.NextNode;
}
if (i == index)
{
var nextNode = node.NextNode == null ? null : node.NextNode;
node.NextNode = new LinkNode<T> { Node = item, NextNode = nextNode };
}
else
{
throw new Exception("Locaion exception");
}
}
public bool IsEmpty()
{
return firstNode == null;
}
public int LocateItem(T t)
{
var index = 0;
var node = firstNode;
while(!node.Node.Equals(t))
{
index++;
if(node.NextNode == null)
{
return -1;
}
node = node.NextNode;
}
return index;
}
}
public class LinkNode<T>
{
public T Node
{
get; set;
}
public LinkNode<T> NextNode
{
get; set;
}
}
其它鏈表
之前說了,如果你想放棄排隊,就要將排在你后面的人的手機號碼告訴前一個人。但是由於不知道排在你前一個人是誰,所以只能去找食堂大媽,食堂大媽去找排在第一個的人,然后通過一層層找下去,才知道你前一個人是誰,這樣就很麻煩,能不能讓你不但知道后一個人的號碼,也知道前一個人的呢?
當然可以,那就是雙向鏈表。
雙向鏈表就是在單鏈表的每個節點中,再設置一個指向其前驅節點的指針域。
用C#代碼表示就是:
public class LinkNode<T>
{
public T Node
{
get; set;
}
public LinkNode<T> NextNode
{
get; set;
}
public LinkNode<T> PrevNode
{
get; set;
}
}
除了雙向鏈表,還有循環鏈表、靜態鏈表等,大家都可以自己實現。
單鏈表結構與順序存儲結構的優缺點
每種數據結構都不是萬能的(不然還要這么多數據結構干嘛),單鏈表和順序存儲結構都有各自的優缺點和適用場景。
順序存儲結構的優缺點
優點:
- 無須為表示表中元素之間的邏輯關系而增加額外的存儲空間(鏈表就需要額外存儲一個指向后繼的指針域)
- 可以快速地存取表中任意位置的元素(直接通過下標返回即可,鏈表需要循環去取)
缺點:
- 插入和刪除操作需要移動大量元素
- 當線性表長度變化較大時,難以確定存儲空間的容量(也可以通過達到一定長度時進行擴容的方式來改善)
- 造成存儲空間的碎片(可以通過少於一定長度時進行壓縮的方式來改善)
單鏈表的優缺點
對於插入或刪除越是頻繁的操作,單鏈表的效率優勢就越是明顯,但是獲取元素的效率非常低。
適用場景
- 若線性表需要頻繁查找,很少進行插入和刪除操作,使用順序存儲結構。若需要頻繁插入和刪除,使用單鏈表結構。
- 若線性表中的元素個數變化較大或者根本不知道有多大時,最好使用單鏈表結構,這樣可以不考慮存儲空間的大小問題。而如果事先知道線性表的長度,使用順序存儲結構效率會高很多。