資料結構基礎溫故-1.線性表(中)
在上一篇中,我們學習了線性表最基礎的表現形式-順序表,但是其存在一定缺點:必須佔用一整塊事先分配好的儲存空間,在插入和刪除操作上需要移動大量元素(即操作不方便),於是不受固定儲存空間限制並且可以進行比較快捷地插入和刪除操作的連結串列橫空出世,所以我們就來複習一下連結串列。
一、單鏈表基礎
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--; } } }View Code
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--; } }View Code
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("