一、什么是單向鏈表
在動態分配內存空間時,最常使用的就是“單向鏈表”(Single Linked List)。一個單向鏈表節點基本上是由兩個元素,即數據字段和指針所組成,而指針將會指向下一個元素在內存中的位置,如下圖所示:
在“單向鏈表”中,第一個節點是“鏈表頭指針”,指向最后一個節點的指針設為NULL,表示它是“鏈表尾”,不指向任何地方。例如列表A={a、b、c、d、x},其單向鏈表的數據結構如下圖所示:
由於單向鏈表中所有節點都知道節點本身的下一個節點在哪里,但是對於前一個節點卻沒有辦法知道,所以在單向鏈表的各種操作中,“鏈表頭指針”就顯得相當重要,只要存在鏈表頭指針,就可以遍歷整個鏈表,進行加入和刪除節點等操作。
注意:除非必要,否則不可移動鏈表頭指針。
通常在其他程序設計語言中,如C或C++語言,是以指針(pointer)類型來處理鏈表類型的數據結構。由於在C#程序設計語言中沒有指針類型,因此可以把鏈表聲明為類(class)。例如要模擬鏈表中的節點,必須聲明如下的Node類,這里使用泛型:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SingleLinkedListDemo { /// <summary> /// 鏈表節點類 /// </summary> public class Node<T> { /// <summary> /// 數據字段 /// </summary> public T Data { get; set; } /// <summary> /// 指針 指向下一個元素 /// </summary> public Node<T> Next { get; set; } /// <summary> /// 無參構造函數 /// </summary> public Node() { // 賦默認值 this.Data = default(T); this.Next = null; } /// <summary> /// 只傳遞數據字段的構造函數,指針默認為null /// </summary> /// <param name="value"></param> public Node(T value) { this.Data = value; this.Next = null; } /// <summary> /// 同時傳遞數據和指針的構造函數 /// </summary> /// <param name="value"></param> /// <param name="next"></param> public Node(T value,Node<T> next) { this.Data = value; this.Next = next; } /// <summary> /// 只傳遞指針的構造函數 /// </summary> /// <param name="next"></param> public Node(Node<T> next) { this.Next = next; } } }
接着可以聲明鏈表SingleLinkedList類,該類定義兩個Node<T>類型的節點指針,分別指向鏈表的第一個節點和最后一個節點:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SingleLinkedListDemo { public class SingleLinkedList<T> { /// <summary> /// 頭節點 /// </summary> private Node<T> HeadNode; /// <summary> /// 尾節點 /// </summary> private Node<T> LastNode; // 定義類中其它方法,增加節點、刪除節點、移動節點等 } }
如果鏈表中的節點不只記錄單一數值,例如每一個節點除了有指向下一個節點的指針字段外,還包括學生的姓名、學號、成績,則其鏈表如下圖所示:
如果是這種鏈表,我們可以先定義一個Student類,里面包括姓名、學號、成績,然后Node節點里面使用泛型Node<Student>。下面我們以學生為例講解如何創建一個單向鏈表。
1、建立單向鏈表
現在我們使用C#語言的鏈表處理以下學生的成績問題。
首先我們必須聲明節點的數據類型,讓每一個節點包含一個數據,並且包含指向下一個節點的指針,使所有的數據都能被串在一起形成一個列表結構,最終鏈表如下圖所示:
下面我們詳細說明建立上圖所示的單向鏈表的步驟:
1、建立一個新的節點,如圖所示:
2、這時鏈表是空的,所以講鏈表的first即last指針字段都指向新創建的節點newNode,如圖所示:
3、建立另外一個新的節點,如圖所示:
4、將上面的兩個節點串起來,使用下面的代碼:
last.next=newNode;
last=newNode;
如圖所示:
5、重復上面的3、4步驟,將所有的節點都連接起來,最終鏈表結構如圖所示:
由於列表中所有節點都知道節點本身的下一個節點在哪里,但是對於前一個節點卻沒有辦法知道,所以“頭節點”就顯得非常重要。
無論如何,只要有頭節點存在,就可以對整個列表進行遍歷、加入、刪除、查找等操作。而之前建立的節點若沒有串接起來就會形成無人管理的節點,並一直占用內存空間。因此在建立列表時必須有一個列表指針指向頭節點,並且在沒有必要的情況下不可以移動列表首指針。
我們可以在程序中會聲明Node類和SignleLinkedList類,在SignleLinkedList類中,定義了兩個Node類型的節點指針,分別指向鏈表的第一個節點和最后一個節點。另外,該類中還需要聲明下面的兩個方法:
方法名稱 | 功能描述 |
public bool IsEmpty() | 用來判斷當前的鏈表是否為空鏈表 |
public void Add(T item) | 用來將指定的節點插入到當前的鏈表 |
下面我們以一個具體的例子來講解如何創建一個單向鏈表。需求如下:
設計一個C#程序,可以讓用戶輸入數據來添加學生數據節點,以建立一個單向鏈表。一共輸入5位學生的成績來建立單向鏈表,然后遍歷單向鏈表中的每一個節點來打印輸出學生的信息。
我們先建立Node節點類,這里使用泛型,利於擴展:
namespace SingleLinkedListDemo { /// <summary> /// 鏈表節點類 /// </summary> public class Node<T> { /// <summary> /// 數據字段 /// </summary> public T Data { get; set; } /// <summary> /// 指針 指向下一個元素 /// </summary> public Node<T> Next { get; set; } /// <summary> /// 無參構造函數 /// </summary> public Node() { // 賦默認值 this.Data = default(T); this.Next = null; } /// <summary> /// 只傳遞數據字段的構造函數,指針默認為null /// </summary> /// <param name="value"></param> public Node(T value) { this.Data = value; this.Next = null; } /// <summary> /// 同時傳遞數據和指針的構造函數 /// </summary> /// <param name="value"></param> /// <param name="next"></param> public Node(T value,Node<T> next) { this.Data = value; this.Next = next; } /// <summary> /// 只傳遞指針的構造函數 /// </summary> /// <param name="next"></param> public Node(Node<T> next) { this.Next = next; } } }
我們創建一個Student類,用來存放學生信息:
namespace SingleLinkedListDemo { /// <summary> /// 學生類 /// </summary> public class Student { /// <summary> /// 學號 /// </summary> public int Number { get; set; } /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 成績 /// </summary> public int Score { get; set; } public Student(int number,string name,int score) { Number = number; Name = name; Score = score; } } }
最后創建SignleLinkedList類,代碼如下:
using System.Collections.Generic; namespace SingleLinkedListDemo { /// <summary> /// 單向鏈表類 /// </summary> /// <typeparam name="T"></typeparam> public class SingleLinkedList<T> { /// <summary> /// 存放所有鏈表節點的集合 /// </summary> public List<Node<T>> ListNode { get; set; } /// <summary> /// 構造函數 /// </summary> public SingleLinkedList() { ListNode = new List<Node<T>>(); } /// <summary> /// 頭節點 /// </summary> private Node<T> HeadNode; /// <summary> /// 尾節點 /// </summary> private Node<T> LastNode; /// <summary> /// 判斷當前鏈表是否為空鏈表 /// </summary> /// <returns></returns> public bool IsEmpty() { return HeadNode == null; } /// <summary> /// 插入節點 /// </summary> public void AddNode(T item) { // 新建一個節點 Node<T> newNode = new Node<T>(item); // 判斷頭節點是否為null,如果為null,那么新建的節點就是頭節點,同時也是尾節點 if (IsEmpty()) { // 如果是空鏈表,則將頭節點和尾節點都指向新建的節點 HeadNode = newNode; LastNode = newNode; } else { // 尾節點的指針指向新建的節點 // 新建的節點變為尾節點 LastNode.Next = newNode; LastNode = newNode; } // 將新建的節點添加到集合中 ListNode.Add(newNode); } } }
在Main方法里面調用:
using System; using System.Collections.Generic; namespace SingleLinkedListDemo { class Program { static void Main(string[] args) { int num; string name; int score; Console.WriteLine("請輸入5位學生的成績:"); SingleLinkedList<Student> linkedList = new SingleLinkedList<Student>(); for (int i = 0; i < 5; i++) { Console.Write("請輸入學號:"); num = int.Parse(Console.ReadLine()); Console.Write("請輸入姓名:"); name = Console.ReadLine(); Console.Write("請輸入成績:"); score = int.Parse(Console.ReadLine()); Student student = new Student(number: num, name: name, score: score); linkedList.AddNode(student); Console.WriteLine("----------------"); } Console.WriteLine(); Console.WriteLine("輸出學生成績信息"); List<Node<Student>> list = linkedList.ListNode; foreach (var item in list) { Console.WriteLine($"學號: {item.Data.Number},姓名: {item.Data.Name},成績: {item.Data.Score}"); Console.WriteLine(); } Console.ReadKey(); } } }
程序運行結果:
2、單向鏈表節點的刪除
在單向鏈表類型的數據結構中,若要在鏈表中刪除一個節點,則根據所刪除節點的位置會有以下三種不同的情況。
1、刪除鏈表的第一個節點
如果是刪除鏈表中的第一個節點,只要把鏈表的頭指針指向第二個節點即可,如圖所示:
程序參考代碼:
if(first.data == delNode.data) first=first.next;
2、刪除鏈表內的中間節點
如果是刪除鏈表內的中間節點,那么只要將刪除節點的前一個節點的指針,指向要刪除節點的下一個節點即可,如圖所示:
程序參考代碼:
3、刪除鏈表的最后一個節點
如果是刪除鏈表的最后一個節點,那么只要將指向最后一個節點的指針,直接指向null即可,如圖所示:
程序參考代碼
我們還是以上面的例子進行講解在鏈表中刪除節點。輸入學號,如果學號存在,則在鏈表中刪除,然后打印出當前鏈表中的節點。如果學號不存在,則給出提示信息。要結束輸入時,請輸入“-1”,此時打印出鏈表中的節點信息。
這時上面創建的泛型類就不符合需求了,我們重新創建一個節點類:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SingleLinkedListDemo { /// <summary> /// 演示刪除節點使用的節點類 /// </summary> public class StudentNode { /// <summary> /// 學號 /// </summary> public int Number { get; set; } /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 成績 /// </summary> public int Score { get; set; } /// <summary> /// 指針 指向下一個元素 /// </summary> public StudentNode Next { get; set; } public StudentNode(int number,string name,int score) { Number = number; Name = name; Score = score; } } }
鏈表類:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SingleLinkedListDemo { public class StudentLinkedList { public List<StudentNode> ListNode { get; set; } public StudentLinkedList() { ListNode = new List<StudentNode>(); } /// <summary> /// 頭節點 /// </summary> public StudentNode HeadNode; /// <summary> /// 尾節點 /// </summary> public StudentNode LastNode; /// <summary> /// 判斷當前鏈表是否為空鏈表 /// </summary> /// <returns></returns> public bool IsEmpty() { return HeadNode == null; } /// <summary> /// 添加節點 /// </summary> /// <param name="node"></param> public void AddNode(StudentNode node) { StudentNode newNode = node; // 判斷頭節點是否為null,如果為null,那么新建的節點就是頭節點,同時也是尾節點 if (IsEmpty()) { // 如果是空鏈表,則將頭節點和尾節點都指向新建的節點 HeadNode = newNode; LastNode = newNode; } else { // 尾節點的指針指向新建的節點 // 新建的節點變為尾節點 LastNode.Next = newNode; LastNode = newNode; } // 將新建的節點添加到集合中 ListNode.Add(newNode); } /// <summary> /// 打印 /// </summary> public void Print() { StudentNode current = HeadNode; while (current != null) { Console.WriteLine("[" + current.Number + " " + current.Name + " " + current.Score + "]"); current = current.Next; } Console.WriteLine(); } /// <summary> /// 刪除節點 /// </summary> /// <param name="delNode"></param> public void DeleteNode(StudentNode delNode) { StudentNode newNode; StudentNode tmpNode; // 如果刪除的是第一個節點 if(HeadNode.Number==delNode.Number) { // 頭指針指向第二個節點 HeadNode = HeadNode.Next; } else if(LastNode.Number==delNode.Number) { // 刪除的是最后一個節點 newNode = HeadNode; // 循環找到最后一個節點的前一個節點 // 當退出循環的時候newNode就是最后一個節點的前一個節點 while(newNode.Next!=LastNode) { // 指針后移,指向下一個節點 newNode = newNode.Next; } // 最后一個節點的前一個節點的next賦值為null newNode.Next = null; LastNode = newNode; } else { // 刪除的是中間的節點 newNode = HeadNode; tmpNode = HeadNode; // 循環找到要刪除的節點 // 循環退出時tmpNode節點就是要刪除節點的前一個節點,newNode節點就是要刪除的節點 while(newNode.Number!=delNode.Number) { tmpNode = newNode; // 后移,指向下一個節點 newNode = newNode.Next; } // 要刪除節點的前一個節點的next指向刪除節點的下一個節點 tmpNode.Next = newNode.Next; } } } }
Main方法里面調用:
using System; using System.Collections.Generic; namespace SingleLinkedListDemo { class Program { static void Main(string[] args) { #region 創建一個單向鏈表 //int num; //string name; //int score; //Console.WriteLine("請輸入5位學生的成績:"); //SingleLinkedList<Student> linkedList = new SingleLinkedList<Student>(); //for (int i = 0; i < 5; i++) //{ // Console.Write("請輸入學號:"); // num = int.Parse(Console.ReadLine()); // Console.Write("請輸入姓名:"); // name = Console.ReadLine(); // Console.Write("請輸入成績:"); // score = int.Parse(Console.ReadLine()); // Student student = new Student(number: num, name: name, score: score); // linkedList.AddNode(student); // Console.WriteLine("----------------"); //} //Console.WriteLine(); //Console.WriteLine("輸出學生成績信息"); //List<Node<Student>> list = linkedList.ListNode; //foreach (var item in list) //{ // Console.WriteLine($"學號: {item.Data.Number},姓名: {item.Data.Name},成績: {item.Data.Score}"); // Console.WriteLine(); //} #endregion #region 刪除單向鏈表中的節點 Random rand = new Random(); StudentLinkedList list = new StudentLinkedList(); int i, j, findword = 0; int[,] data = new int[12, 10]; String[] name = new String[] { "Allen", "Scott", "Marry", "Jon", "Mark", "Ricky", "Lisa", "Jasica", "Hanson", "Amy", "Bob", "Jack" }; Console.WriteLine("學號 成績 學號 成績 學號 成績 學號 成績\n "); // 鏈表里面添加12個節點 for (i = 0; i < 12; i++) { data[i, 0] = i + 1; data[i, 1] = (Math.Abs(rand.Next(50))) + 50; StudentNode node = new StudentNode(data[i, 0], name[i], data[i, 1]); list.AddNode(node); } // 分三行輸出 for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) Console.Write("[" + data[j * 3 + i, 0] + "] [" + data[j * 3 + i, 1] + "] "); Console.WriteLine(); } while (true) { Console.Write("請輸入要刪除成績的學生學號,結束輸入-1: "); findword = int.Parse(Console.ReadLine()); if (findword == -1) break; else { StudentNode current = new StudentNode(list.HeadNode.Number, list.HeadNode.Name, list.HeadNode.Score); current.Next = list.HeadNode.Next; while (current.Number != findword) current = current.Next; list.DeleteNode(current); } Console.WriteLine("刪除后成績的鏈表,請注意!要刪除的成績其學生的學號必須在此鏈表中\n"); list.Print(); } #endregion Console.ReadKey(); } } }
程序運行結果:
3、單向鏈表插入新節點
在單向鏈表中插入新節點,如同一列火車中加入新的車廂,有三種情況:加到第一個節點之前、加到最后一個節點之后以及加到此鏈表中間任一位置。
1、新節點插入第一個節點之前
將新節點插入到第一個節點之前,新節點即成為此鏈表的首節點,只需要把新節點的指針指向鏈表原來的第一個節點,再把鏈表頭指針指向新節點即可,如圖所示:
2、新節點插入最后一個節點之后
將新節點插入到最后一個節點之后,只需要把鏈表的最后一個節點的指針指向新節點,新節點的指針在指向null即可,如圖所示:
3、新節點插入鏈表中間位置
例如插入的節點是在X和Y之間,只需要將X節點的指針指向新節點,如圖所示:
然后將新節點的指針指向Y節點即可,如圖所示:
下面我們以泛型Node類為例,講解如何插入節點
using System; using System.Collections.Generic; namespace SingleLinkedListDemo { /// <summary> /// 單向鏈表類 /// </summary> /// <typeparam name="T"></typeparam> public class SingleLinkedList<T> { /// <summary> /// 存放所有鏈表節點的集合 /// </summary> public List<Node<T>> ListNode { get; set; } /// <summary> /// 構造函數 /// </summary> public SingleLinkedList() { ListNode = new List<Node<T>>(); } /// <summary> /// 頭節點 /// </summary> private Node<T> HeadNode; /// <summary> /// 尾節點 /// </summary> private Node<T> LastNode; /// <summary> /// 判斷當前鏈表是否為空鏈表 /// </summary> /// <returns></returns> public bool IsEmpty() { return HeadNode == null; } /// <summary> /// 新增節點 /// </summary> public void AddNode(T item) { // 新建一個節點 Node<T> newNode = new Node<T>(item); // 判斷頭節點是否為null,如果為null,那么新建的節點就是頭節點,同時也是尾節點 if (IsEmpty()) { // 如果是空鏈表,則將頭節點和尾節點都指向新建的節點 HeadNode = newNode; LastNode = newNode; } else { // 尾節點的指針指向新建的節點 // 新建的節點變為尾節點 LastNode.Next = newNode; LastNode = newNode; } // 將新建的節點添加到集合中 ListNode.Add(newNode); } /// <summary> /// 插入節點 /// </summary> /// <param name="item">要插入的節點的值</param> /// <param name="index">要插入節點的位置</param> public void InsertNode(T item,int index) { // 創建新的節點 Node<T> newNode = new Node<T>(item); //Node<T> tmpNode = new Node<T>(item); // 判斷當前鏈表是否為空鏈表 if(IsEmpty()) { HeadNode = newNode; LastNode = newNode; } else { // 插入第一個節點 if(index==0) { // 新節點執行現在的頭節點 newNode.Next = HeadNode; // 新節點變為新的頭節點 HeadNode = newNode; } else if(index==GetLinkedListLength()-1) { // 插入尾節點 // 定義一個臨時節點tempNode指向HeadNode Node<T> tempNode = HeadNode; // 循環找到尾節點 while(true) { // 如果tempNode的next不為null,說明當前節點不是尾節點,則后移 if(tempNode.Next!=null) { // 當前tempNode后移 tempNode = tempNode.Next; } else { // tempNode的next為null,說明tempNode節點是尾節點,則退出循環 break; } } // tempNode是尾節點,則將尾節點的next指向新的節點 tempNode.Next = newNode; } else { #region 插入中間位置 // 定義臨時節點指向頭節點 Node<T> tempNode = HeadNode; // 經過index-1次循環后移,tempNode移動到要插入位置的前一個節點 for (int i = 0; i <=index-1; i++) { // tempNode節點每次后移一個位置 tempNode = tempNode.Next; } // 要插入位置的前一個節點 Node<T> preNode = tempNode; // 要插入位置的節點 Node<T> currentNode = preNode.Next; // 修改next指向,前一個節點指向新節點 preNode.Next = newNode; // 新節點指向當前位置的節點 newNode.Next = currentNode; #endregion } } } /// <summary> /// 獲取鏈表長度 /// </summary> /// <returns></returns> public int GetLinkedListLength() { // 長度 int length = 0; if(HeadNode==null) { length = 0; } else { Node<T> tempNode = HeadNode; // 循環 while(true) { if(tempNode.Next!=null) { // 當前臨時節點后移到下一個節點 tempNode = tempNode.Next; // 長度自增 length++; } else { // 說明循環到了尾節點,退出循環 length++; break; } } } return length; } /// <summary> /// 打印 /// </summary> public void Print() { //StudentNode current = HeadNode; //while (current != null) //{ // Console.WriteLine("[" + current.Number + " " + current.Name + " " + current.Score + "]"); // current = current.Next; //} Node<T> current = HeadNode; while (current != null) { Console.Write( current.Data+" "); current = current.Next; } Console.WriteLine(); } } }
Main方法中調用:
using System; using System.Collections.Generic; namespace SingleLinkedListDemo { class Program { static void Main(string[] args) { #region 創建一個單向鏈表 //int num; //string name; //int score; //Console.WriteLine("請輸入5位學生的成績:"); //SingleLinkedList<Student> linkedList = new SingleLinkedList<Student>(); //for (int i = 0; i < 5; i++) //{ // Console.Write("請輸入學號:"); // num = int.Parse(Console.ReadLine()); // Console.Write("請輸入姓名:"); // name = Console.ReadLine(); // Console.Write("請輸入成績:"); // score = int.Parse(Console.ReadLine()); // Student student = new Student(number: num, name: name, score: score); // linkedList.AddNode(student); // Console.WriteLine("----------------"); //} //Console.WriteLine(); //Console.WriteLine("輸出學生成績信息"); //List<Node<Student>> list = linkedList.ListNode; //foreach (var item in list) //{ // Console.WriteLine($"學號: {item.Data.Number},姓名: {item.Data.Name},成績: {item.Data.Score}"); // Console.WriteLine(); //} #endregion #region 刪除單向鏈表中的節點 //Random rand = new Random(); //StudentLinkedList list = new StudentLinkedList(); //int i, j, findword = 0; //int[,] data = new int[12, 10]; //String[] name = new String[] { "Allen", "Scott", // "Marry", "Jon", "Mark", "Ricky", "Lisa", // "Jasica", "Hanson", "Amy", "Bob", "Jack" }; //Console.WriteLine("學號 成績 學號 成績 學號 成績 學號 成績"); //// 鏈表里面添加12個節點 //for (i = 0; i < 12; i++) //{ // data[i, 0] = i + 1; // data[i, 1] = (Math.Abs(rand.Next(50))) + 50; // StudentNode node = new StudentNode(data[i, 0], name[i], data[i, 1]); // list.AddNode(node); //} //// 分三行輸出 //for (i = 0; i < 3; i++) //{ // for (j = 0; j < 4; j++) // Console.Write("[" + data[j * 3 + i, 0] + "] [" + data[j * 3 + i, 1] + "] "); // Console.WriteLine(); //} //while (true) //{ // Console.Write("請輸入要刪除成績的學生學號,結束輸入-1: "); // findword = int.Parse(Console.ReadLine()); // if (findword == -1) // break; // else // { // StudentNode current = new StudentNode(list.HeadNode.Number, list.HeadNode.Name, list.HeadNode.Score); // current.Next = list.HeadNode.Next; // while (current.Number != findword) current = current.Next; // list.DeleteNode(current); // } // Console.WriteLine("刪除后成績的鏈表,請注意!要刪除的成績其學生的學號必須在此鏈表中\n"); // list.Print(); //} #endregion #region 單向鏈表中插入節點 SingleLinkedList<int> linkedList = new SingleLinkedList<int>(); linkedList.AddNode(1); linkedList.AddNode(45); linkedList.AddNode(56); linkedList.AddNode(389); List<Node<int>> list = linkedList.ListNode; Console.WriteLine("插入前鏈表元素"); linkedList.Print(); Console.WriteLine(); // 插入頭節點之前 linkedList.InsertNode(57, 0); Console.WriteLine("插入頭節點后鏈表元素"); linkedList.Print(); Console.WriteLine(); // 插入尾節點之后 linkedList.InsertNode(123, linkedList.GetLinkedListLength()-1); Console.WriteLine("插入尾節點后鏈表元素"); linkedList.Print(); Console.WriteLine(); // 插入中間節點 int index= new Random().Next(0, linkedList.GetLinkedListLength() - 1); linkedList.InsertNode(935, index); Console.WriteLine("插入中間節點后鏈表元素"); linkedList.Print(); Console.WriteLine(); #endregion Console.ReadKey(); } } }
程序運行結果:
GitHub地址:git@github.com:JiangXiaoLiang1988/SingleLinkedList.git