如何实现双向循环链表

博主主页:17_Kevin-CSDN博客

收录专栏:《数据结构》


引言

双向带头循环链表是一种常见的数据结构,它具有双向遍历的特性,并且在表头和表尾之间形成一个循环。本文将深入探讨双向带头循环链表的结构、操作和应用场景,帮助读者更好地理解和运用这一数据结构。

本篇博客将以图表和代码相结合的方式手撕双向带头循环链表,代码使用C语言进行实现。

1. 结构的定义

双向带头循环链表由多个节点组成,每个节点包含数据域和两个指针域,分别指向前驱节点(prev)和后继节点(next)。在链表的表头和表尾之间会形成一个循环,使得链表可以从任意节点出发进行正向或反向的遍历。

typedef struct ListNode
{struct ListNode* next;struct ListNode* prev;LTDataType data;
}ListNode;

通过代码可以感受到,每一个链表节点都包括一个prev和一个next(除了哨兵节点),整体结构的示意图如下:

2. 基本操作

2.1 准备操作

2.1.1 创建新节点

ListNode* BuyListNode(LTDataType x)
{ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}

每个节点都应该具备data,next,prev这三个结构体成员,结构的定义在上文已经进行了描述,所以在创建新节点中直接用ListNode*类型进行新节点的创建。参数x表示要在新节点上插入的数据,在创建新节点后对新节点的成员进行初始化,最后返回ListNode*类型的newnode。

2.1.2 初始化链表 

ListNode* ListInit()
{ListNode* phead = BuyListNode(0);phead->next = phead;phead->prev = phead;return phead;
}

在刚开始的时候需要对链表进行初始化。我们要实现的是一个双向带头循环链表,所以在初始化的时候使哨兵节点的next指向自己,prev指向自己,这样的结构对后面对链表的操作会方便很多,提供了很大的便利。

2.2 遍历操作

2.2.1 打印链表

void ListPrint(ListNode* phead)
{assert(phead);ListNode* cur = phead->next;while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}

 打印链表不仅可以实现最后链表结果的输出,也可以让我们在进行链表代码书写的时候进行检查所写接口是否有误。

在实现打印链表的时候我们先用一个assert断言来进行判断,如果phead使空的话就会报错停止运行,因为至少要保证有一个表头,要不然无法组成链表。

我们使用一个指针cur来进行访问链表,初始化cur指向phead的next,这样就指向了第一个节点,从第一个节点开始遍历,之后用while循环来进行遍历,每次循环打印当前cur的data,使cur指向cur的next,也就指向了下一个节点。终止的条件是:当cur指向phead的时候终止循环。

2.2.2 查找数据位置

ListNode* ListFind(ListNode* phead, LTDataType x)
{assert(phead);ListNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

首先用assert断言确保该链表不是空的,以保证可以正确查询。定义一个指针cur指向哨兵节点的next(第一个节点),然后循环遍历,直到cur对应的data为要查找的x值的时候停止循环,返回存储x的节点,如果未找到则返回NULL。通过此操作即可找到要查找的数据的位置。

2.3 插入操作

在表头插入的时候有链接新节点的顺序需要注意,有以下两种,第一种为指针方法忽视链接顺序,第二种为直接链接新节点,需要注意链接顺序。

2.3.1.1 在表头插入新节点(first指针)

void ListPushFront(ListNode* phead, LTDataType x)
{assert(phead);ListNode* first = phead->next;ListNode* newnode = BuyListNode(x);// phead newnode firstphead->next = newnode;newnode->prev = phead;newnode->next = first;first->prev = newnode;
}

 2.3.1.2 在表头插入新节点(顺序链接)

void ListPushFront(ListNode* phead, LTDataType x)
{assert(phead);ListNode* newnode = BuyListNode(x);newnode->next = phead->next;phead->next->prev = newnode;phead->next = newnode;newnode->prev = phead;
}

以上为两个表头插入接口函数,明显的区别就是第一种使用first进行保存表头的next,之后在连接的时候使用first就可以进行正常链接。第二种中直接进行链接,但是这种需要注意链接的顺序,因为程序的编译是从上向下进行编译,所以在链接时的顺序不当可能使本该链接的地址被修改覆盖,造成错误的链接,使插入节点新失败。

2.3.2 在表尾插入新节点

void ListPushBack(ListNode* phead, LTDataType x)
{assert(phead);ListNode* tail = phead->prev;ListNode* newnode = BuyListNode(x);tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;
}

由于哨兵节点的结构有前驱节点和后继节点,所以在循环带头双向链表中哨兵节点的前驱节点就是最后一个节点的后继节点。我们用tail表示链表的最后一个节点,使tail指向表头的前驱节点,这样就可以快速定位到最后一个节点的next,以便于用来拼接新节点。之后就是使表头和当前的tail与新节点的前驱节点和后继节点进行拼接。

2.3.3 在指定位置插入新节点

// pos位置之前插入x
void ListInsert(ListNode* pos, LTDataType x)
{assert(pos);ListNode* prev = pos->prev;ListNode* newnode = BuyListNode(x);// prev newnode posprev->next = newnode;//pos前的节点的nextnewnode->prev = prev;newnode->next = pos;pos->prev = newnode;
}

用该接口在pos位置前插入新节点。因为NULL没有前后两个指针域,为了避免pos是NULL所以我们使用assert断言进行判断,避免出错。定义一个prev表示pos前的节点,然后用prev链接newnode,再用newnode链接pos,这样就完成了在pos前插入数据了。

2.4 删除操作

2.4.1 删除表头节点

void ListPopFront(ListNode* phead)
{assert(phead);assert(phead->next != phead);ListNode* first = phead->next;ListNode* second = first->next;phead->next = second;second->prev = phead;free(first);first = NULL;
}

该接口中一共使用了两次assert断言:

  1. assert(phead);
  2. assert(phead->next != phead);

 第一个assert用来放置表头为NULL,第二个assert是避免链表不存在数据还进行删除,因为当链表中只存在哨兵节点的时候它的next是指向它自己的,所以使用的条件是phead的next不等于phead。

因为我们是从表头删除节点,所以我们可以先通过哨兵节点找到第一个节点,然后再找到第二个节点。我们的目的是将第一个节点删除,所以我们先定义一个指针first然后用first先暂时存贮第一个节点,然后通过first找到第二个节点,最后再用phead的next与第二个节点进行链接(free掉first节省空间)。如此便实现表头删除节点的接口。

2.4.2 删除表尾节点

void ListPopBack(ListNode* phead)
{assert(phead);assert(phead->next != phead);ListNode* tail = phead->prev;ListNode* prev = tail->prev;prev->next = phead;phead->prev = prev;free(tail);tail = NULL;
}

该接口的两个assert和上方表头删除节点的原理相同,不做过多讲解。

循环链表的表尾就是表头的prev,所以很简单就可以将tail表示出来。再定义一个prev指针用来存储要删除的尾的前一个节点位置。在完成准备工作后我们使用prev的next跳过tail直接指向phead,然后在将phead的prev指向prev。这样就完成了表尾节点的删除,最后用free将之前的表尾节点释放掉就更完美啦!

2.4.3 删除指定位置节点

// 删除pos位置的值
void ListErase(ListNode* pos)
{assert(pos);ListNode* prev = pos->prev;ListNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);
}

该节点用来删除指定位置的节点。

首先使用assert断言保证该位置不是NULL,可以真实进行修改。

例如说,我们要删除d2节点:

按照代码逻辑d2就是传入的参数pos,现在我们定义一个指针prev指向d2的prev,也就是d1,再定义一个指针next指向d2的next,也就是d3。

这样我们就拥有了prev和next两个分别指向目标节点前后节点的指针,然后通过这两个这两个指针将d1和d3进行链接就完成了删除d2的操作,当然,最后将d2给free掉就更完美啦~


通过本文的介绍,我们对双向带头循环链表有了更深入的了解,包括其结构、基本操作、应用场景以及示例代码。双向带头循环链表作为一种重要的数据结构,在实际开发中有着广泛的应用,希望本文能够帮助读者更好地理解和应用这一数据结构。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/707737.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【医学影像】LIDC-IDRI数据集的无痛制作

LIDC-IDRI数据集制作 0.下载0.0 链接汇总0.1 步骤 1.合成CT图reference 0.下载 0.0 链接汇总 LIDC-IDRI官方网址:https://www.cancerimagingarchive.net/nbia-search/?CollectionCriteriaLIDC-IDRINBIA Data Retriever 下载链接:https://wiki.canceri…

Golang使用Swag搭建api文档

1. 简介 Gin是Golang目前最为常用的Web框架之一。 公司项目验收需要API接口设计说明书(Golang后端服务基于Gin框架编写),编写任务自然就落到了我们研发人员身上。 项目经理提供了文档模板,让我们参考模板来手动编写,要…

5.WEB渗透测试-前置基础知识-常用的dos命令

内容参考于: 易锦网校会员专享课 上一篇内容:4.WEB渗透测试-前置基础知识-快速搭建渗透环境(下)-CSDN博客 常用的100个CMD指令 1.gpedit.msc—–组策略 2. sndrec32——-录音机 3. Nslookup——-IP地址侦测器 ,是一个…

Unity中的UI系统之GUI

目录 概述工作原理和主要作用基础控件重要参数及文本和按钮多选框和单选框输入框和拖动条图片绘制和框 复合控件工具栏和选择网络滚动视图和分组窗口 自定义整体样式自定义皮肤样式 概述 什么是UI系统 UI是User Interface(用户界面)的简称,用…

全域增长方法论:帮助品牌实现科学经营,助力长效生意增长

前两年由于疫情反复、供给需求收缩等条件制约,品牌业务均受到不同程度的影响。以双十一和618电商大促为例,就相比往年颇显“惨淡”,大多品牌营销都无法达到理想预期。 随着市场环境不断开放,2023年营销行业开始从低迷期走上了高速…

SDWAN异地组网难在哪?怎么解决?

SD-WAN作为一种先进的网络技术,为企业提供了更加灵活和高效的网络连接方案。然而,在异地组网的过程中,SD-WAN也面临一些挑战。本文将探讨SD-WAN异地组网所面临的难题,并提供相应的解决方案。 挑战一:网络延迟和不稳定性…

Jupyter Notebook 下载+简单设置

这里写目录标题 1. Jupyter Notebook安装2.切换打开别的盘3. 创建代码文件4.为jupyter notebook添加目录 (Jupyter安装拓展nbextensions)step1:安装命令step2:用户配置step3:上述过程均完成后,打开jupyter notebook就会发现界面多…

常见的socket函数封装和多进程和多线程实现服务器并发

常见的socket函数封装和多进程和多线程实现服务器并发 1.常见的socket函数封装2.多进程和多线程实现服务器的并发2.1多进程服务器2.2多线程服务器2.3运行效果 1.常见的socket函数封装 accept函数或者read函数是阻塞函数,会被信号打断,我们不能让它停止&a…

哪里申请EV代码签名证书?

EV代码签名证书是一种高级别的数字证书,它通过严格的验证流程,确保软件发布者身份的真实性和可信度。相较于普通代码签名证书,EV证书采用了更严格的验证标准,包括对企业身份、法律地位、组织结构多个方面的核实。这使得EV证书成为…

06 Qt自绘组件:Switch动画开关组件

系列文章目录 01 Qt自定义风格控件的基本原则-CSDN博客 02 从QLabel聊起:自定义控件扩展-图片控件-CSDN博客 03 从QLabel聊起:自定义控件扩展-文本控件-CSDN博客 04 自定义Button组件:令人抓狂的QToolButton文本图标居中问题-CSDN博客 0…

js中的任务处理机制

众所周知(不知道的话去查),js是以单线程的方式执行的,在执行的过程中,某一时刻上只能执行一个任务,也就是说,我们写好了代码后执行的时候,程序是根据代码从上到下依次排队执行,只有上一个任务执…

数据分析案例-社交媒体情绪数据集可视化分析(文末送书)

🤵‍♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞&#x1f4…

进程的控制

文章目录 进程退出进程等待进程程序替换 正文开始前给大家推荐个网站,前些天发现了一个巨牛的 人工智能学习网站, 通俗易懂,风趣幽默,忍不住分享一下给大家。 点击跳转到网站。 进程退出 进程的退出一共有三种场景。 程序跑完…

别再让机会从指缝间溜走!社科院与杜兰大学金融管理硕士一同开创你的成功之路

新的一年,你的读研计划进行到哪个环节了呢?咨询社科院与杜兰大学金融管理硕士项目中,总听到有同学说,不着急,我先了解一下。你不知道是时间总是在指缝间溜走。别让犹豫成了我们前进的阻碍,马上行动早日遇到…

基于Springboot的计算机知识竞赛网站(有报告)。Javaee项目,springboot项目。

演示视频: 基于Springboot的计算机知识竞赛网站(有报告)。Javaee项目,springboot项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结…

产品老化试验目的、用途

什么是老化试验? 老化试验是通过模拟产品在使用过程中的老化情况,来评估产品在长期使用后的性能和可靠性。这种测试可以帮助制造商了解产品的寿命和耐久性,以及产品在不同环境条件下的表现。 模拟量采集/老化房采集软件 为什么需要进行老化试…

智慧公厕:让城市更智慧、更环保

在现代社会,智慧公厕作为城市管理的重要一环,是智慧城市的重要组成部分,其建设的价值十出突出,是公共厕所信息化升级改造的核心方案。如智慧公厕源头厂家广州中期科技有限公司,所自主研发的智慧公厕整体解决方案&#…

【HarmonyOS】鸿蒙开发之Video组件——第3.7章

Video组件内VideoOptions属性简介 src:设置视频地址。currentProgressRate:设置视频播放倍速,参数说明如下: number|string:只支持 0.75 , 1.0 , 1.25 , 1.75 , 2.0 。P…

安装 docker 可视化工具 portainer

portainer 官方网站 https://www.portainer.io/ 一、portainer 介绍 Portainer是一款开源的容器管理平台,它提供了一个直观易用的Web界面,帮助用户管理Docker容器集群、镜像、卷等资源。Portainer 支持多种 Docker 环境,包括本地Docker、Sw…

智能手表的革命性突破:TRIZ理论引领未来穿戴技术!

在科技日新月异的今天,智能手表已经从单纯的计时工具转变为集健康监测、信息通讯、娱乐休闲等多功能于一体的智能穿戴设备。而基于TRIZ理论的智能手表更是在这一变革中扮演着引领者的角色。TRIZ,即发明问题解决理论,是一套系统的创新方法学&a…