链表(二):单向链表
一、什么是单向链表
在动态分配内存空间时,最常使用的就是“单向链表”(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