链表(二):双向链表
一、双向链表
单向链表和环形链表都是属于拥有方向性的链表,只能单向遍历,万一不幸其中有一个链接断裂,那么后面的链表数据便会遗失而无法复原了。因此,我们可以将两个方向不同的链表结合起来,除了存放数据的字段以外,它还有两个指针变量,其中一个指针指向后面的节点,另一个则指向前面的节点,这样的链表被称为双向链表(Double Linked List)。
由于每个节点都有两个指针,可以双向通行,因此能够轻松地找到前后节点,同时从链表中任意的节点也可以找到其他节点,而不需要经过反转或对比节点等处理,执行速度较快。另外,如果任一节点的链接断裂,可通过反方向链表进行遍历,从而快速地重建完整的链表。
双向链表的最大优点是有两个指针分别指向节点前后两个节点,所以能够轻松地找到前后节点,同时从双向链表中任一节点也可以找到其他节点,而不需要经过反转或对比节点等处理,执行速度较快。
双向链表的缺点是由于双向链表有两个链接,所以在加入或删除节点时都要花更多时间来调整指针。另外因为每个节点含有两个指针变量,所以较浪费空间。
二、双向链表的定义
下面来介绍双向链表的数据结构。对每个节点而言,具有三个字段,中间为数据字段,左右两边各有两个链表字段,分表为LLink和RLink,其中LLink指向前一个节点,RLink指向后一个节点,如图所示:
在双向链表中,通常加上一个链表头,该链表节点不存放任何数据,其左链接字段指向链表的最后一个节点,而右链接指向第一个节点。
如果使用C#语言来声明双向链表节点的数据结构,那么其声明的程序代码如下:
namespace DoubleLinkedListDemo { public class Node { /// <summary> /// 数据字段 /// </summary> public int Data { get; set; } /// <summary> /// 后续节点 /// </summary> public Node NextNode { get; set; } /// <summary> /// 前置节点 /// </summary> public Node PreNode { get; set; } public Node(int data) { Data = data; NextNode = null; PreNode = null; } } }
1、双向链表节点的插入
双向链表节点的插入有下面三种情况。
1、新节点插入到链表的第一个节点前
将新节点插入到链表的第一个节点前分为如下的步骤:
- 将新节点的右指针指向原链表的第一个节点。
- 将原链表第一个节点的左指针指向新节点。
- 将原链表的表头指针指向新节点,且新节点的左指针指向null。
如图所示:
2、新节点插入此链表的末尾
将新节点插入到此链接的末尾分为如下步骤:
- 将原链表的最后一个节点的右指针指向新节点。
- 将新节点的左指针指向原链表的最后一个节点,并将新节点的右指针指向null。
如图所示:
3、新节点插入到中间节点
将新节点插入到中间节点(ptr指向的节点)之后,分为如下步骤:
- 将ptr节点的右指针指向新节点。
- 将新节点的左指针指向ptr节点。
- 将ptr节点的下一个节点的左指针指向新节点。
- 将新节点的右指针指向ptr的下一个节点。
如图所示:
2、双向链表节点的删除
双向链表的节点删除,同样也有下面的三种情况。
1、删除双向链表的第一个节点
将双向链表的第一个节点删除分为如下步骤:
- 将链表头指针head指向原链表的第二个节点。
- 将新的链表头指针指向null。
如图所示:
2、删除双向链表的最后一个节点
删除双向链表的最后一个节点只需要将原链表最后一个节点的前一个节点的右指针指向null即可,如图所示:
3、删除双向链表的中间节点
假设ptr指向的是双向链表的中间节点,删除该节点,分为如下步骤:
- 将ptr节点的前一个节点的右指针指向ptr节点的下一个节点。
- 将ptr节点的下一个节点的左指针指向ptr节点的前一个节点。
如图所示:
下面我们以具体的代码演示双向链表的数据结构、创立、插入和删除节点,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DoubleLinkedListDemo { /// <summary> /// 双向链表类 /// </summary> public class DoubleLinkedList { /// <summary> /// 第一个节点 /// </summary> public Node HeadNode; /// <summary> /// 尾节点 /// </summary> public Node LastNode; /// <summary> /// 判断当前双向链表是否为空链表 /// </summary> /// <returns></returns> public bool IsEmpty() { return HeadNode == null; } /// <summary> /// 打印出节点的数据字段 /// </summary> public void Print() { Node currentNode = HeadNode; while(currentNode!=null) { Console.Write($"{currentNode.Data} "); currentNode = currentNode.NextNode; } Console.WriteLine(); } /// <summary> /// 新增节点,用来创建一个双向链表,每次都是插入到最后一个节点之后 /// </summary> /// <param name="node"></param> public void AddNode(Node newNode) { if(HeadNode==null) { HeadNode = newNode; LastNode = newNode; HeadNode.NextNode = LastNode; LastNode.PreNode = HeadNode; LastNode.NextNode = null; } else { LastNode.NextNode = newNode; newNode.PreNode = LastNode; LastNode = newNode; //Node temp = HeadNode; //// 循环找到最后一个节点,然后添加 //// 退出循环时temp就是最后一个节点 //while(true) //{ // if(temp.NextNode!=null) // { // // 后移节点 // temp = temp.NextNode; // } // else // { // // 退出循环 // break; // } //} //// 将新节点插入到最后一个节点之后 //temp.NextNode = newNode; //newNode.PreNode = temp; // // LastNode = newNode; } } public void InsertNode(int item,int index) { Node newNode = new Node(item); // 插入头部位置 if(index==0) { HeadNode.PreNode = newNode; newNode.NextNode = HeadNode; HeadNode = newNode; } else if(index == GetLinkedList()-1) { // 插入尾部位置 LastNode.NextNode = newNode; newNode.PreNode = LastNode; LastNode = newNode; } else { // tempNode就是要插入的节点位置 Node tempNode = HeadNode; // 插入中间位置 for (int i = 0; i < GetLinkedList()-1; i++) { if(i!=index) { tempNode = tempNode.NextNode; } else { break; } } newNode.PreNode = tempNode; newNode.NextNode = tempNode.NextNode; tempNode.NextNode.PreNode = newNode; tempNode.NextNode = newNode; } } /// <summary> /// 获取链表长度 /// </summary> /// <returns></returns> public int GetLinkedList() { int length = 0; if(HeadNode==null) { length = 0; } else { Node tempNode = HeadNode; while(true) { if(tempNode.NextNode!=null) { tempNode = tempNode.NextNode; length++; } else { length++; break; } } } return length; } } }
Main方法中调用:
using System; namespace DoubleLinkedListDemo { class Program { static void Main(string[] args) { Node node1 = new Node(1); Node node2 = new Node(34); Node node3 = new Node(564); Node node4 = new Node(81); Node node5 = new Node(6); DoubleLinkedList dLinkedList = new DoubleLinkedList(); dLinkedList.AddNode(node1); dLinkedList.AddNode(node2); dLinkedList.AddNode(node3); dLinkedList.AddNode(node4); dLinkedList.AddNode(node5); Console.WriteLine("插入前链表"); dLinkedList.Print(); // 插入头部 dLinkedList.InsertNode(29, 0); Console.WriteLine("插入头部后链表"); dLinkedList.Print(); // 插入尾部 dLinkedList.InsertNode(724, dLinkedList.GetLinkedList() - 1); Console.WriteLine("插入尾部后链表"); dLinkedList.Print(); // 插入中间节点 int index = new Random().Next(1, dLinkedList.GetLinkedList()); dLinkedList.InsertNode(34242, index); Console.WriteLine("插入中间位置后链表"); dLinkedList.Print(); Console.ReadKey(); } } }
程序运行结果:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
2017-02-16 JavaScript:DOM操作