带头双向循环链表的实现

news/2025/1/1 7:52:22/

目录

  • 前言
  • 节点声明
  • 链表的初始化
  • 尾插
  • 打印链表
  • 头插
  • 尾删
  • 头删
  • 查找节点
  • 指定位置插入
  • 指定位置删除
  • 链表销毁

前言

之前讲过单链表的实现,在实现的过程中,我们会发现每次删除或者在前面插入节点的时候,都要提前保存上一个节点的地址。这样做十分麻烦,所以就有了单链表的升级,双链表。今天就来实现一个带头双向循环的双链表。

带头: 指有一个哨兵位的头节点,头节点不拿来存放有效值
双向: 代表是个双向链表,一个指针存放前一个节点地址,一个地址存放后一个节点地址。
循环: 最后一个节点的下一个节点是头节点。
在这里插入图片描述

节点声明

刚刚提到过,双向链表是有2个指针的,一个指针指向前一个节点,一个指针指向后一个节点,还有一个来存放数据。

//存放的类型
typedef int ListValType;//节点结构体声明
typedef struct ListNode
{ListValType val;struct ListNode* prve;struct ListNode* next;
}LSNode;

链表的初始化

因为我们是带哨兵头的链表,所以链表需要初始化。
初始化我们只需要开辟一个头节点,但这个节点我们不存放有效值。
而因为是循环链表,所以最后一个节点要指向第一个节点,只有一个节点时,自己指向自己。
在这里插入图片描述
初始化代码

//链表初始化
LSNode* ListInto()
{//创建一个节点LSNode* newNode = (LSNode*)malloc(sizeof(LSNode));//指向自己newNode->next = newNode;newNode->prve = newNode;return newNode;
}

在这里插入图片描述
调试后发现它的next和prve指针都指向自己,说明初始化完成了。

尾插

那么我们就可以写个尾部插入来玩玩,我们都知道头节点的前一个地址是指向最后一个地址的。所以就可以直接找到尾节点进行插入。
在这里插入图片描述


//创建节点
LSNode* CreateListNode(ListValType x)
{LSNode* newNode = (LSNode*)malloc(sizeof(LSNode));if (newNode == NULL){//空间开辟失败,不玩了exit(-1);}newNode->val = x;return newNode;
}//尾插
void ListPushBack(LSNode* phead, ListValType x)
{//断言,phead不能为空assert(phead);//创建一个新节点LSNode* newNode = CreateListNodeC(x);//记录头节点的前一个节点,也就是尾节点LSNode* tail = phead->prve;//尾节点指向 新节点tail->next = newNode;//新节点前节点指向尾节点newNode->prve = tail;//后节点指向头节点newNode->next = phead;//头节点前节点指向新节点phead->prve = newNode;}

然后调试发现链表已经连起来了
在这里插入图片描述

打印链表

为了方便测试,我们对链表进行打印一下,但这次打印和之前不同,因为之前是打印到最后一个节点为NULL停止,但循环链表不存在NULL节点,所以当我们再次走到头节点的时候停止打印。

//打印链表
void ListPrint(LSNode* phead)
{assert(phead);//头节点存放的是无效值,所以从头节点下一个位置开始打印LSNode* cru = phead->next;//cru == phead时说明链表走了一圈了while (cru != phead){printf("%d->", cru->val);cru = cru->next;}printf(".....\n");
}

在这里插入图片描述
因为是循环链表,所以不可能打印完,我们打印一圈就可以了。

头插

头插也很简单,因为头节点存放的不是有效值,我们只需要记录头节点的下一个节点,然后让它的 prve节点指向新节点,让新节点next节点指向它,头节点的next指向新节点,新节点的prve节点指向头节点。
语言表达有点绕,看图。
在这里插入图片描述
代码

//头插
void ListPushFront(LSNode* phead, ListValType x)
{//断言,phead不能为空assert(phead);//创建一个新节点LSNode* newNode = CreateListNode(x);//保存头节点的下一个节点LSNode* Next = phead->next;//next前节点指向新节点Next->prve = newNode;//新节点next指向NextnewNode->next = Next;//头节点next指向新节点phead->next = newNode;//新节点前节点指向头节点newNode->prve = phead;
}

在这里插入图片描述

尾删

尾删和尾插差不多,不过需要最后一个节点的前一个节点,然后让它指向头节点,随后释放最后一个节点。
在这里插入图片描述
但是我们需要注意一个问题,当尾节点等于头节点时,那么说明链表没有节点了,哨兵头节点是不能删除的,所以这时我们需要判断或者断言一下都行。

//尾删
void ListPopBack(LSNode* phead)
{//phead不能为空assert(phead);//尾节点不能头节点一样assert(phead != phead->prve);//尾节点LSNode* tail = phead->prve;//尾节点的前一个节点LSNode* prvetail = tail->prve;//释放尾节点free(tail);//prvetail 连接 头节点prvetail->next = phead;phead->prve = prvetail;}

在这里插入图片描述

然后我们发现后面的3和2都被删掉了

头删

头删我们只需要记录头节点的下一个节点的下一个节点,因为等等头节点要和这个节点连接,然后释放掉头节点的下一个节点即可。
在这里插入图片描述

当然,头删和尾删一样,当链表只剩下头节点时,那就不能再删除了。

//头删
void ListPopFront(LSNode* phead)
{assert(phead);assert(phead != phead->prve);//头节点的下一个节点LSNode* headNext = phead->next;//下下个节点LSNode* nextnext = headNext->next;//连接头节点和 nextnextnextnext->prve = phead;phead->next = nextnext;//释放headNextfree(headNext);headNext = NULL;
}

代码执行结果
在这里插入图片描述

查找节点

这个就很简单了,思路和打印差不多,遍历一遍找,没找到返回空指针。

//查找
LSNode* ListFindNode(LSNode* phead, ListValType x)
{assert(phead);//头节点存放的是无效值,所以从头节点下一个位置开始查找LSNode* cru = phead->next;//cru == phead时说明链表走了一圈了while (cru != phead){if (cru->val == x)return cru;cru = cru->next;}return NULL;
}

在这里插入图片描述

指定位置插入

指定位置插入和头插尾插没有太大区别,如果前插,保存pos位置的前一个节点,如果后插,保存后一个节点,然后连接。
这里我们演示前插。在这里插入图片描述

//指定插入
void ListInsert(LSNode* phead, LSNode* pos, ListValType x)
{//pos和phead不能为空assert(pos && phead);//创建节点LSNode* newNode = CreateListNode(x);//保存pos的前一个节点LSNode* posprve = pos->prve;//然后连接起来posprve->next = newNode;newNode->prve = posprve;newNode->next = pos;pos->prve = newNode;}

在这里插入图片描述
我们发现这个函数也可以完成头插和尾插,所以说前面的头插和尾插可以直接复用这个函数。
头插更新

//头插
void ListPushFront(LSNode* phead, ListValType x)
{/*//断言,phead不能为空assert(phead);//创建一个新节点LSNode* newNode = CreateListNode(x);//保存头节点的下一个节点LSNode* Next = phead->next;//next前节点指向新节点Next->prve = newNode;//新节点next指向NextnewNode->next = Next;//头节点next指向新节点phead->next = newNode;//新节点前节点指向头节点newNode->prve = phead;*/ListInsert(phead,phead->next,x);
}

尾插更新

//尾插
void ListPushBack(LSNode* phead, ListValType x)
{/*//断言,phead不能为空assert(phead);//创建一个新节点LSNode* newNode = CreateListNode(x);//记录头节点的前一个节点,也就是尾节点LSNode* tail = phead->prve;//尾节点指向 新节点tail->next = newNode;//新节点前节点指向尾节点newNode->prve = tail;//后节点指向头节点newNode->next = phead;//头节点前节点指向新节点phead->prve = newNode;*/ListInsert(phead, phead->prve, x);}

指定位置删除

前删的话我们首先保存pos的前一个节点和后一个节点,然后两个节点连接,释放pos即可,也可以先释放,没有顺序要求。
在这里插入图片描述

//指定删除
void ListEase(LSNode* phead, LSNode* pos)
{//pos和phead不能为空assert(pos && phead);//保存pos前后节点LSNode* posprve = pos->prve;LSNode* posnext = pos->next;//前后节点连接posprve->next = posnext;posnext->prve = posprve;//释放pos空间free(pos);pos = NULL;
}

在这里插入图片描述
我们发现它同样可以完成 头删和尾删,所以头删和尾删我们可以直接复用这个函数。
头删更新

//头删
void ListPopFront(LSNode* phead)
{/*assert(phead);assert(phead != phead->prve);//头节点的下一个节点LSNode* headNext = phead->next;//下下个节点LSNode* nextnext = headNext->next;//连接头节点和 nextnextnextnext->prve = phead;phead->next = nextnext;//释放headNextfree(headNext);headNext = NULL;*/ListEase(phead, phead->next);
}

尾删更新

void ListPopBack(LSNode* phead)
{/*//phead不能为空assert(phead);//尾节点不能头节点一样assert(phead != phead->prve);//尾节点LSNode* tail = phead->prve;//尾节点的前一个节点LSNode* prvetail = tail->prve;//释放尾节点free(tail);//prvetail 连接 头节点prvetail->next = phead;phead->prve = prvetail;*/ListEase(phead, phead->prve);
}

链表销毁

销毁是连头也一起销毁的,所以先保存后一个或前一个地址,然后释放当前地址,然后迭代一个个销毁,直到最后遇到头节点后销毁头节点并结停止迭代在这里插入图片描述

//销毁链表,因为会改变原来的phead节点,所以需要传二级指针
void ListDestroy(LSNode** pphead)
{assert(*pphead);LSNode* cru = (*pphead)->next;while (1){//保存后一个节点LSNode* next = cru->next;//释放crufree(cru);//迭代cru = next;//如果cru 和头节点相等,说明头节点后面的都删完了if (cru == *pphead){//释放头节点空间free(*pphead);//指针置空*pphead = NULL;//跳出循环break;	}}
}

在这里插入图片描述

我们在调试看看是否真的销毁成功了。
在这里插入图片描述
这样我们的带头双向循环链表已经简单实现完了。代码已上传至git,点此获取

这里还有单链表的实现,有兴趣的大佬也可以看看


http://www.ppmy.cn/news/1237.html

相关文章

计算机系统基础实验——数据的机器级表示(计算浮点数 f 的绝对值[f])

题目要求: 这个函数计算浮点数f的绝对值[f]。如果f是NaN,函数应该简单的返回f。 Unsigned float_abs (unsiged f) { /**************/ return/*******/; } 先分析题目,题目有两个要求: 1.判断f是否是NAN类型,如果是返…

【java】网络编程

文章目录网络编程概述基本概念IP地址概念InetAddress端口与协议概念UDP通信编程UDP发送数据UDP接受数据UDP通信程序练习TCP通信编程TCP发送数据TCP接收数据TCP通信程序练习网络编程概述 基本概念 IP地址概念 终端检查: InetAddress package heima.网络编程;impor…

SpringBoot+Vue项目餐饮管理系统

文末获取源码 开发语言:Java 使用框架:spring boot 前端技术:JavaScript、Vue.js 、css3 开发工具:IDEA/MyEclipse/Eclipse、Visual Studio Code 数据库:MySQL 5.7/8.0 数据库管理工具:phpstudy/Navicat JD…

Redis客户端常见异常

客户端读写超时 读写超时时间设置得过短命令本身就比较慢客户端与服务端网络不正常redis自身发生堵塞 客户端连接超时 连接超时时间设置过短redis发生阻塞,造成tcp-backlog 已满,造成新的连接失败客户端与服务端网络不正常 客户端缓冲区异常 输出缓…

[附源码]计算机毕业设计疫情期间小学生作业线上管理系统Springboot程序

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

【经验分享】突然我的SM.MS的图床没法访问了(内附解决方法)

【经验分享】突然我的SM.MS的图床没法访问了(内附解决方法) 一大早写文章,发现Markdown里的图片全部都不能成功加载了,这个的确挺头疼的! 文章目录1 说一说现象2 简单排查一下3 查找解决方案4 实施解决方案5 总结6 更多…

双十二电容笔哪个品牌好?十大电容笔知名品牌

现在,电容笔的普及度和性能都在不断提高。而如何选择一款性价比高的电容笔,则成为了一个很大的难题。很多人把电容笔作为日常使用的工具,因此,大家都在寻找更好,更经济的电容笔。所以,哪个品牌的电容笔最便…

Java并发编程—java异步Future的迭代过程

在我们java多线程中,我想做一件事儿,但是我又不想影响主线程的执行,很多铁子都会想到用异步任务完成,这个时候我们的主角FutureTask就登场了。 一、FutureTask介绍 FutureTask提供了对Future的基本实现,是一个可取消的…