在上一篇中,我們學習了線性表最基礎的表現形式-順序表,但是其存在一定缺點:必須占用一整塊事先分配好的存儲空間,在插入和刪除操作上需要移動大量元素(即操作不方便),於是不受固定存儲空間限制並且可以進行比較快捷地插入和刪除操作的鏈表橫空出世,所以我們就來復習一下鏈表。
一、單鏈表基礎
1.1 單鏈表的節點結構
在鏈表中,每個節點由兩部分組成:數據域和指針域。
1.2 單鏈表的總體結構
鏈表就是由N個節點鏈接而成的線性表,如果其中每個節點只包含一個指針域那么就稱為單鏈表,如果含有兩個指針域那么就稱為雙鏈表。
PS:在線性表的鏈式存儲結構中,為了便於插入和刪除操作的實現,每個鏈表都帶有一個頭指針(或尾指針),通過頭指針可以唯一標識該鏈表。從頭指針所指向的節點出發,沿着節點的鏈可以訪問到每個節點。
二、單鏈表實現
2.1 單鏈表節點的定義
public class Node<T> { // 數據域 public T Item { get; set; } // 指針域 public Node<T> Next { get; set; } public Node() { } public Node(T item) { this.Item = item; } }
此處定義Node類為單鏈表的節點,其中包括了一個數據域Item與一個指針域Next(指向后繼節點的位置)。
2.2 單鏈表節點的新增
①默認在尾節點后插入新節點
public void Add(T value) { Node<T> newNode = new Node<T>(value); if (this.head == null) { // 如果鏈表當前為空則置為頭結點 this.head = newNode; } else { Node<T> prevNode = this.GetNodeByIndex(this.count - 1); prevNode.Next = newNode; } this.count++; }
首先判斷頭結點是否為空,其次依次遍歷各節點找到尾節點的前驅節點,然后更改前驅節點的Next指針指向新節點即可。
②指定在某個節點后插入新節點
public void Insert(int index, T value) { Node<T> tempNode = null; if (index < 0 || index > this.count) { throw new ArgumentOutOfRangeException("index", "索引超出范圍"); } else if (index == 0) { if (this.head == null) { tempNode = new Node<T>(value); this.head = tempNode; } else { tempNode = new Node<T>(value); tempNode.Next = this.head; this.head = tempNode; } } else { Node<T> prevNode = GetNodeByIndex(index - 1); tempNode = new Node<T>(value); tempNode.Next = prevNode.Next; prevNode.Next = tempNode; } this.count++; }
這里需要判斷是否是在第一個節點進行插入,如果是則再次判斷頭結點是否為空。
2.3 單鏈表節點的移除
public void RemoveAt(int index) { if (index == 0) { this.head = this.head.Next; } else { Node<T> prevNode = GetNodeByIndex(index - 1); if (prevNode.Next == null) { throw new ArgumentOutOfRangeException("index", "索引超出范圍"); } Node<T> deleteNode = prevNode.Next; prevNode.Next = deleteNode.Next; deleteNode = null; } this.count--; }
移除某個節點只需將其前驅節點的Next指針指向要移除節點的后繼節點即可。
至此,關鍵部分的代碼已介紹完畢,下面給出完整的單鏈表模擬實現代碼:

/// <summary> /// 單鏈表模擬實現 /// </summary> public class MySingleLinkedList<T> { private int count; // 字段:當前鏈表節點個數 private Node<T> head; // 字段:當前鏈表的頭結點 // 屬性:當前鏈表節點個數 public int Count { get { return this.count; } } // 索引器 public T this[int index] { get { return this.GetNodeByIndex(index).Item; } set { this.GetNodeByIndex(index).Item = value; } } public MySingleLinkedList() { this.count = 0; this.head = null; } // Method01:根據索引獲取節點 private Node<T> GetNodeByIndex(int index) { if (index < 0 || index >= this.count) { throw new ArgumentOutOfRangeException("index", "索引超出范圍"); } Node<T> tempNode = this.head; for (int i = 0; i < index; i++) { tempNode = tempNode.Next; } return tempNode; } // Method02:在尾節點后插入新節點 public void Add(T value) { Node<T> newNode = new Node<T>(value); if (this.head == null) { // 如果鏈表當前為空則置為頭結點 this.head = newNode; } else { Node<T> prevNode = this.GetNodeByIndex(this.count - 1); prevNode.Next = newNode; } this.count++; } // Method03:在指定位置插入新節點 public void Insert(int index, T value) { Node<T> tempNode = null; if (index < 0 || index > this.count) { throw new ArgumentOutOfRangeException("index", "索引超出范圍"); } else if (index == 0) { if (this.head == null) { tempNode = new Node<T>(value); this.head = tempNode; } else { tempNode = new Node<T>(value); tempNode.Next = this.head; this.head = tempNode; } } else { Node<T> prevNode = GetNodeByIndex(index - 1); tempNode = new Node<T>(value); tempNode.Next = prevNode.Next; prevNode.Next = tempNode; } this.count++; } // Method04:移除指定位置的節點 public void RemoveAt(int index) { if (index == 0) { this.head = this.head.Next; } else { Node<T> prevNode = GetNodeByIndex(index - 1); if (prevNode.Next == null) { throw new ArgumentOutOfRangeException("index", "索引超出范圍"); } Node<T> deleteNode = prevNode.Next; prevNode.Next = deleteNode.Next; deleteNode = null; } this.count--; } } }
2.4 單鏈表的模擬實現簡單測試
這里針對模擬的單鏈表進行三個簡單的測試:一是順序插入4個節點;二是在指定的位置插入單個節點;三是移除指定位置的單個節點;測試代碼如下所示:
static void MySingleLinkedListTest() { MySingleLinkedList<int> linkedList = new MySingleLinkedList<int>(); // Test1:順序插入4個節點 linkedList.Add(0); linkedList.Add(1); linkedList.Add(2); linkedList.Add(3); Console.WriteLine("The nodes in the linkedList:"); for (int i = 0; i < linkedList.Count; i++) { Console.WriteLine(linkedList[i]); } Console.WriteLine("----------------------------"); // Test2.1:在索引為0(即第1個節點)的位置插入單個節點 linkedList.Insert(0, 10); Console.WriteLine("After insert 10 in index of 0:"); for (int i = 0; i < linkedList.Count; i++) { Console.WriteLine(linkedList[i]); } // Test2.2:在索引為2(即第3個節點)的位置插入單個節點 linkedList.Insert(2, 20); Console.WriteLine("After insert 20 in index of 2:"); for (int i = 0; i < linkedList.Count; i++) { Console.WriteLine(linkedList[i]); } // Test2.3:在索引為5(即最后一個節點)的位置插入單個節點 linkedList.Insert(5, 30); Console.WriteLine("After insert 30 in index of 5:"); for (int i = 0; i < linkedList.Count; i++) { Console.WriteLine(linkedList[i]); } Console.WriteLine("----------------------------"); // Test3.1:移除索引為5(即最后一個節點)的節點 linkedList.RemoveAt(5); Console.WriteLine("After remove an node in index of 5:"); for (int i = 0; i < linkedList.Count; i++) { Console.WriteLine(linkedList[i]); } // Test3.2:移除索引為0(即第一個節點)的節點 linkedList.RemoveAt(0); Console.WriteLine("After remove an node in index of 0:"); for (int i = 0; i < linkedList.Count; i++) { Console.WriteLine(linkedList[i]); } // Test3.3:移除索引為2(即第三個節點)的節點 linkedList.RemoveAt(2); Console.WriteLine("After remove an node in index of 2:"); for (int i = 0; i < linkedList.Count; i++) { Console.WriteLine(linkedList[i]); } Console.WriteLine("----------------------------"); } #endregion }
測試結果如下圖所示:
①順序插入4個新節點
②在指定位置插入新節點
③在指定位置移除某個節點
三、雙鏈表基礎
3.1 雙鏈表的節點結構
與單鏈表不同的是,雙鏈表有兩個指針域,一個指向前驅節點,另一個指向后繼節點。
3.2 雙鏈表的總體結構
雙鏈表中,每個節點都有兩個指針,指向前驅和后繼,這樣可以方便地找到某個節點的前驅節點和后繼節點,這在某些場合中是非常實用的。
四、雙鏈表實現
4.1 雙鏈表節點的定義
public class DbNode<T> { public T Item { get; set; } public DbNode<T> Prev { get; set; } public DbNode<T> Next { get; set; } public DbNode() { } public DbNode(T item) { this.Item = item; } }
與單鏈表的節點定義不同的是,多了一個指向前驅節點的Prev指針域,可以方便地找到某個節點的前驅節點,從而不必重新遍歷一次。
4.2 雙鏈表中插入新節點
①默認在尾節點之后插入新節點
public void AddAfter(T value) { DbNode<T> newNode = new DbNode<T>(value); if (this.head == null) { // 如果鏈表當前為空則置為頭結點 this.head = newNode; } else { DbNode<T> lastNode = this.GetNodeByIndex(this.count - 1); // 調整插入節點與前驅節點指針關系 lastNode.Next = newNode; newNode.Prev = lastNode; } this.count++; }
②可選在尾節點之前插入新節點
public void AddBefore(T value) { DbNode<T> newNode = new DbNode<T>(value); if (this.head == null) { // 如果鏈表當前為空則置為頭結點 this.head = newNode; } else { DbNode<T> lastNode = this.GetNodeByIndex(this.count - 1); DbNode<T> prevNode = lastNode.Prev; // 調整倒數第2個節點與插入節點的關系 prevNode.Next = newNode; newNode.Prev = prevNode; // 調整倒數第1個節點與插入節點的關系 lastNode.Prev = newNode; newNode.Next = lastNode; } this.count++; }
典型的四個步驟,調整插入節點與尾節點前驅節點的關系、插入節點與尾節點的關系。
當然,還可以在指定的位置之前或之后插入新節點,例如InsertAfter和InsertBefore方法,代碼詳見下面4.3后面的完整實現。
4.3 雙鏈表中移除某個節點
public void RemoveAt(int index) { if (index == 0) { this.head = this.head.Next; } else { DbNode<T> prevNode = this.GetNodeByIndex(index - 1); if (prevNode.Next == null) { throw new ArgumentOutOfRangeException("index", "索引超出范圍"); } DbNode<T> deleteNode = prevNode.Next; DbNode<T> nextNode = deleteNode.Next; prevNode.Next = nextNode; if(nextNode != null) { nextNode.Prev = prevNode; } deleteNode = null; } this.count--; }
這里只需要將前驅節點的Next指針指向待刪除節點的后繼節點,將后繼節點的Prev指針指向待刪除節點的前驅節點即可。
至此,關鍵部分的代碼已介紹完畢,下面給出完整的雙鏈表模擬實現代碼:

/// <summary> /// 雙鏈表的模擬實現 /// </summary> public class MyDoubleLinkedList<T> { private int count; // 字段:當前鏈表節點個數 private DbNode<T> head; // 字段:當前鏈表的頭結點 // 屬性:當前鏈表節點個數 public int Count { get { return this.count; } } // 索引器 public T this[int index] { get { return this.GetNodeByIndex(index).Item; } set { this.GetNodeByIndex(index).Item = value; } } public MyDoubleLinkedList() { this.count = 0; this.head = null; } // Method01:根據索引獲取節點 private DbNode<T> GetNodeByIndex(int index) { if (index < 0 || index >= this.count) { throw new ArgumentOutOfRangeException("index", "索引超出范圍"); } DbNode<T> tempNode = this.head; for (int i = 0; i < index; i++) { tempNode = tempNode.Next; } return tempNode; } // Method02:在尾節點后插入新節點 public void AddAfter(T value) { DbNode<T> newNode = new DbNode<T>(value); if (this.head == null) { // 如果鏈表當前為空則置為頭結點 this.head = newNode; } else { DbNode<T> lastNode = this.GetNodeByIndex(this.count - 1); // 調整插入節點與前驅節點指針關系 lastNode.Next = newNode; newNode.Prev = lastNode; } this.count++; } // Method03:在尾節點前插入新節點 public void AddBefore(T value) { DbNode<T> newNode = new DbNode<T>(value); if (this.head == null) { // 如果鏈表當前為空則置為頭結點 this.head = newNode; } else { DbNode<T> lastNode = this.GetNodeByIndex(this.count - 1); DbNode<T> prevNode = lastNode.Prev; // 調整倒數第2個節點與插入節點的關系 prevNode.Next = newNode; newNode.Prev = prevNode; // 調整倒數第1個節點與插入節點的關系 lastNode.Prev = newNode; newNode.Next = lastNode; } this.count++; } // Method04:在指定位置后插入新節點 public void InsertAfter(int index, T value) { DbNode<T> tempNode; if (index == 0) { if (this.head == null) { tempNode = new DbNode<T>(value); this.head = tempNode; } else { tempNode = new DbNode<T>(value); tempNode.Next = this.head; this.head.Prev = tempNode; this.head = tempNode; } } else { DbNode<T> prevNode = this.GetNodeByIndex(index); // 獲得插入位置的節點 DbNode<T> nextNode = prevNode.Next; // 獲取插入位置的后繼節點 tempNode = new DbNode<T>(value); // 調整插入節點與前驅節點指針關系 prevNode.Next = tempNode; tempNode.Prev = prevNode; // 調整插入節點與后繼節點指針關系 if (nextNode != null) { tempNode.Next = nextNode; nextNode.Prev = tempNode; } } this.count++; } // Method05:在指定位置前插入新節點 public void InsertBefore(int index, T value) { DbNode<T> tempNode; if (index == 0) { if (this.head == null) { tempNode = new DbNode<T>(value); this.head = tempNode; } else { tempNode = new DbNode<T>(value); tempNode.Next = this.head; this.head.Prev = tempNode; this.head = tempNode; } } else { DbNode<T> nextNode = this.GetNodeByIndex(index); // 獲得插入位置的節點 DbNode<T> prevNode = nextNode.Prev; // 獲取插入位置的前驅節點 tempNode = new DbNode<T>(value); // 調整插入節點與前驅節點指針關系 prevNode.Next = tempNode; tempNode.Prev = prevNode; // 調整插入節點與后繼節點指針關系 tempNode.Next = nextNode; nextNode.Prev = tempNode; } this.count++; } // Method06:移除指定位置的節點 public void RemoveAt(int index) { if (index == 0) { this.head = this.head.Next; } else { DbNode<T> prevNode = this.GetNodeByIndex(index - 1); if (prevNode.Next == null) { throw new ArgumentOutOfRangeException("index", "索引超出范圍"); } DbNode<T> deleteNode = prevNode.Next; DbNode<T> nextNode = deleteNode.Next; prevNode.Next = nextNode; if(nextNode != null) { nextNode.Prev = prevNode; } deleteNode = null; } this.count--; } }
4.4 雙鏈表模擬實現的簡單測試
這里跟單鏈表一樣,進行幾個簡單的測試:一是順序插入(默認在尾節點之后)4個新節點,二是在尾節點之前和在指定索引位置插入新節點,三是移除指定索引位置的節點,四是修改某個節點的Item值。測試代碼如下所示。
static void MyDoubleLinkedListTest() { MyDoubleLinkedList<int> linkedList = new MyDoubleLinkedList<int>(); // Test1:順序插入4個節點 linkedList.AddAfter(0); linkedList.AddAfter(1); linkedList.AddAfter(2); linkedList.AddAfter(3); Console.WriteLine("The nodes in the DoubleLinkedList:"); for (int i = 0; i < linkedList.Count; i++) { Console.Write(linkedList[i] + " "); } Console.WriteLine(); Console.WriteLine("----------------------------"); // Test2.1:在尾節點之前插入2個節點 linkedList.AddBefore(10); linkedList.AddBefore(20); Console.WriteLine("After add 10 and 20:"); for (int i = 0; i < linkedList.Count; i++) { Console.Write(linkedList[i] + " "); } Console.WriteLine(); // Test2.2:在索引為2(即第3個節點)的位置之后插入單個節點 linkedList.InsertAfter(2, 50); Console.WriteLine("After add 50:"); for (int i = 0; i < linkedList.Count; i++) { Console.Write(linkedList[i] + " "); } Console.WriteLine(); // Test2.3:在索引為2(即第3個節點)的位置之前插入單個節點 linkedList.InsertBefore(2, 40); Console.WriteLine("After add 40:"); for (int i = 0; i < linkedList.Count; i++) { Console.Write(linkedList[i] + " "); } Console.WriteLine(); Console.WriteLine("----------------------------"); // Test3.1:移除索引為7(即最后一個節點)的位置的節點 linkedList.RemoveAt(7); Console.WriteLine("After remove an node in index of 7:"); for (int i = 0; i < linkedList.Count; i++) { Console.Write(linkedList[i] + " "); } Console.WriteLine(); // Test3.2:移除索引為0(即第一個節點)的位置的節點的值 linkedList.RemoveAt(0); Console.WriteLine("After remove an node in index of 0:"); for (int i = 0; i < linkedList.Count; i++) { Console.Write(linkedList[i] + " "); } Console.WriteLine(); // Test3.3:移除索引為2(即第3個節點)的位置的節點 linkedList.RemoveAt(2); Console.WriteLine("After remove an node in index of 2:"); for (int i = 0; i < linkedList.Count; i++) { Console.Write(linkedList[i] + " "); } Console.WriteLine(); Console.WriteLine("----------------------------"); // Test4:修改索引為2(即第3個節點)的位置的節點的值 linkedList[2] = 9; Console.WriteLine("After update the value of node in index of 2:"); for (int i = 0; i < linkedList.Count; i++) { Console.Write(linkedList[i] + " "); } Console.WriteLine(); Console.WriteLine("----------------------------"); }
測試結果如下圖所示。
五、.NET中的ListDictionary與LinkedList<T>
在.NET中,已經為我們提供了單鏈表和雙鏈表的實現,它們分別是ListDictionary與LinkedList<T>。從名稱可以看出,單鏈表的實現ListDictionary不是泛型實現,而LinkedList是泛型實現,它們又到底有什么區別呢,借助Reflector去看看吧。
5.1 ListDictionary—基於key/value的單鏈表
ListDictionary位於System.Collection.Specialized下,它是基於鍵值對(Key/Value)的集合,微軟給出的建議是:通常用於包含10個或10個以下項的集合。
它的節點的數據域是一個鍵值對,而不是一個簡單的value。
5.2 LinkedList—神奇的泛型雙向鏈表
在.NET中,LinkedList<T>是使用地比較多的鏈表實現類,它位於System.Collections.Generic下,是一個通用的雙向鏈表類,它不支持隨機訪問(即索引訪問),但它實現了很多的新增節點的方法,例如:AddAfter、AddBefore、AddFirst以及AddLast等。其中,AddAfter是在現有節點之后添加新節點,AddBefore則是在現有節點之前添加新節點,AddFirst是在開頭處添加,而AddLast則是在末尾處添加。
參考資料
(1)程傑,《大話數據結構》
(2)陳廣,《數據結構(C#語言描述)》
(3)段恩澤,《數據結構(C#語言版)》
(4)率輝,《數據結構高分筆記(2015版)》