数据结构入门指南:带头双向循环链表

news/2024/5/21 0:14:39/文章来源:https://blog.csdn.net/2202_75605090/article/details/132105235

目录

文章目录

前言

1.结构与优势

2.链表实现      

2.1 定义链表

2.2 创建头节点

2.3 尾插

2.4 输出链表

2.5 尾删

2.6 头插

2.7头删

2.8 节点个数

2.9 查找

2.10 位置插入

2.11 位置删除

2.12 销毁链表

 3. 源码

总结


前言

        链表一共有8种结构,但最常用的就是无头单向链表、和带头双向循环链表。单链表的结构存在着很多的缺陷,但它是许多数据结构的子结构,在刷题中经常见到,而带头双向循环链表弥补了单链表所有的缺陷,可以说是一个完美结构,虽然相对于单链表来说结构更复杂,但它的特性使它的实现逻辑较为简单,今天我就向大家一一介绍。


1.结构与优势

结构

优势

  1. 可以实现快速的插入和删除操作:由于链表的特性,插入和删除节点的时间复杂度为O(1),不需要像数组一样需要移动其他元素。
  2. 可以实现快速的遍历操作:双向链表可以通过前向或后向的指针进行遍历,而不需要像单向链表那样只能从头节点开始遍历。
  3. 可以实现双向遍历:双向链表可以通过前向和后向的指针进行双向遍历,可以方便地从任意一个节点开始向前或向后遍历。
  4. 可以实现循环遍历:由于链表的循环性质,可以通过不断移动指针进行循环遍历,不需要额外的循环条件判断。
  5. 可以实现更多高级功能:带头双向循环链表可以实现更多高级功能,如反转链表、查找倒数第k个节点等。

总结,带头双向循环链表具有灵活性和高效性,适用于需要频繁插入、删除和遍历操作的场景。

2.链表实现      

 2.1 定义链表

        步入正题,带头双向循环链表,首先它是双向的,什么是双向呢?在单链表定义时会有指向下一个节点的指针,而双向链表有两个指针,一个指向下一个节点,一个指向上一个节点,可以实现前后双向遍历。

typedef struct ListNode
{int data;struct ListNode* next;//指向下一个节点的指针struct ListNode* prev;//指向上一个节点的指针
}LN;

 2.2 创建头节点

         和无头单向链表不同,带头双向循环链表它有头节点,所以在开始执行各操作之前,我们先创建一个头节点并初始化。

        创建头节点需要新建一个节点,在其他插入接口中也需要新建节点,那我们就封装一个创建新节点的函数,最后返回新建节点的地址。

LN* BuyListNode(Datatype x)
{LN* newnode = (LN*)malloc(sizeof(LN));if (newnode == NULL){perror("malloc");exit(-1);}newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}

         和无头单链表不同,这里带有头节点,就需要对头节点进行初始化一下,虽然在创建节点时就已经对节点进行了初始化,但头节点的初始化与新建节点初始化需求不同所有这里需要单独进行初始化初始化节点时可以使用双指针,也可以直接返回头节点地址。

LN* InItNode()
{LN* phead = BuyListNode(-1);phead->next = phead;phead->prev = phead;return phead;
}

         头节点进行初始化时,只需将两个指针指向自己,然后返回头节点的地址即可。

 2.3 尾插

        建好头节点后要怎么链接节点呢?我们先写一个尾插来体验一下它的便捷。在单链表中想要进行尾插,还需要遍历一遍链表找到尾节点,而带头双向循环链表就不需要,可以通过头节点直接找到尾节点:tail  =  phead -> prev ;头节点的前一个节点就是尾节点。

我们新建一个节点:

         要想插入就需要把尾节点的next改为新节点的地址,新节点的prev改为尾节点tail的地址

        再把新节点的next改为头节点的地址,把头节点的prev改位新节点的地址。

 

 整体逻辑就是这样,具体代码如下:

void PushBack(LN* phead, Datatype x)
{assert(phead);LN* tail = phead->prev;LN* newnode = BuyListNode(x);tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;
}

2.4 输出链表

        为了便于后续的测试,我们先写一个函数用于输出链表数据。输出函数很简单。

void PrintList(LN* phead)
{assert(phead);LN* cur = phead->next;//有效节点不包含头节点printf("phead <->");while (cur != phead){printf(" %d <->", cur->data);cur = cur->next;}
}

         这里需要注意的是遍历链表时的循环条件,由于它是循环链表,所有结束条件有所变化。其次是输出数据时不需要输出头节点的数据,头节点的下一个节点才是有效数据。

我们可以来测试一下:

void test1()
{LN* plist = InItNode();PushBack(plist, 1);PushBack(plist, 2);PushBack(plist, 3);PushBack(plist, 4);PushBack(plist, 5);PrintList(plist);
}int main()
{test1();return 0;
}

 输出效果如下:

 2.5 尾删

        基本逻辑理解后,可以先自主尝试写一下尾删。思路也是非常的简单,但要注意的是,有效节点为0的情况。把尾节点的前一个节点next改为头节点地址,头节点的prev改为尾节点的前一个节点,最后释放掉尾节点的空间。

void PopBack(LN* phead)
{assert(phead);assert(phead->next != phead);LN* tail = phead->prev;LN* tailprev = tail->prev;tailprev->next = phead;phead->prev = tailprev;free(tail);
}

2.6 头插

        接下来我们进行头插操作,我们使用的是带头的形式,所有这里进行头插时,不像单链表那样需要使用二级指针,我们需要改的是头节点中的内容,所有使用一级指针就可以解决。

         此外头插时一定要注意操作的次序,避免后续操作有误。例如:

        如果不提前保存第一个节点的地址, 就会导致新节点链接后续节点时找节点麻烦或出现错误

 正确的顺序如下图:

 代码实现:

void PushFront(LN* phead,Datatype x)
{assert(phead);LN* newnode = BuyListNode(x);newnode->next = phead->next;phead->next->prev = newnode;newnode->prev = phead;phead->next = newnode;}

         当然新手不建议这样写,这样写很容易把人搞晕,我们可以先保存第一个节点的地址,这样就不会搞错。代码如下:

void PushFront(LN* phead,Datatype x)
{assert(phead);LN* newnode = BuyListNode(x);LN* first = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = first;first->prev = newnode;}

2.7头删

        这里头删也是非常简单,为了避免出错,我们可以额外创建两个指针,记录第一个节点和第二个节点,逻辑较为简单,就不再画逻辑图,代码如下:

void PopFront(LN* phead)
{assert(phead);assert(phead->next != phead);LN* first = phead->next;LN* second = first->next;free(first);phead->next = second;second->prev = phead;}

需要额外注意链表为空的情况。

2.8 节点个数

        统计节点个数很简单,和输出链表数据一样遍历一下链表统计链表个数代码如下:

int Listsize(LN* phead)
{assert(phead);int sz = 0;LN* cur = phead->next;while (cur != phead){sz++;cur = cur->next;}return sz;
}

         有人可能会想:用头节点统计不也可以吗?还不用额外的去写一个函数。初始时把头节点的data初始化为0,每次插入data++。这种方式严格来说是不可以的,我们在写链表时每个节点不一定存储的是整形,也可能是字符型,虽然也能计数,但如果节点是1000呢?数据就溢出了,所以不建议那样写。

 2.9 查找

        查找也比较简单,不再多说,代码如下:

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

 2.10 位置插入

         带头双向循环链表在位置插入时没有像单链表那样有位置前插入,位置后插入。这里的位置插入都是位置前插入,由于它是循环双向的链表,不像单链表那样不可以向前遍历,双向循环链表可以任意插入,所以位置后插入也并没有什么意义,就统一默认位置前插入。

        位置插入操作和上述的插入操作很类似,推荐额外创建一个指针保存节点信息,就可以避免操作时次序不当造成程序错误,代码如下:

void ListInsert(LN* pos, Datatype x)
{assert(pos);LN* newnode = BuyListNode(x);LN* posprev = pos->prev;posprev->next = newnode;newnode->prev = posprev;newnode->next = pos;pos->prev = newnode;}

2.11 位置删除

        位置删除也一样很简单,我们多创建两个指针变量存储节点信息,就可以有效避免次序不当导致程序错误的问题。代码如下:

void PosErase(LN* pos)
{assert(pos);LN* posprev = pos->prev;LN* posnext = pos->next;free(pos);posprev->next = posnext;posnext->prev = posprev;
}

 2.12 销毁链表

        在链表使用完以后就要销毁,销毁也比较简单,代码如下:

void DestoryList(LN* phead)
{assert(phead);LN* cur = phead->next;while (cur != phead){LN* next = cur->next;free(cur);cur = next;}free(phead);
}

         这样还需要注意的一点,在销毁链表的这个函数里虽然销毁了头节点,但是在头节点创建之初使用了结构体指针,在后续操作中都是通过这个指针将链表传递给函数,所以最后在调用销毁链表函数之后要将指向头节点的指针置为NULL,避免出现野指针。当然这里也可以使用二级指针在函数里将这个指针置为NULL。

 3. 源码

List.c

#include"List.h"
LN* BuyListNode(Datatype x)
{LN* newnode = (LN*)malloc(sizeof(LN));if (newnode == NULL){perror("malloc");exit(-1);}newnode->data = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}
LN* InItNode()
{LN* phead = BuyListNode(-1);phead->next = phead;phead->prev = phead;return phead;
}
void PrintList(LN* phead)
{assert(phead);LN* cur = phead->next;printf("phead <->");while (cur != phead){printf(" %d <->", cur->data);cur = cur->next;}printf("\n");
}
void PushBack(LN* phead, Datatype x)
{assert(phead);LN* tail = phead->prev;LN* newnode = BuyListNode(x);tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;
}
void PopBack(LN* phead)
{assert(phead);assert(phead->next != phead);LN* tail = phead->prev;LN* tailprev = tail->prev;tailprev->next = phead;phead->prev = tailprev;free(tail);
}
void PushFront(LN* phead,Datatype x)
{assert(phead);/*LN* newnode = BuyListNode(x);newnode->next = phead->next;phead->next->prev = newnode;newnode->prev = phead;phead->next = newnode;*/LN* newnode = BuyListNode(x);LN* first = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next = first;first->prev = newnode;}
void PopFront(LN* phead)
{assert(phead);assert(phead->next != phead);LN* first = phead->next;LN* second = first->next;free(first);phead->next = second;second->prev = phead;}
int Listsize(LN* phead)
{assert(phead);int sz = 0;LN* cur = phead->next;while (cur != phead){sz++;cur = cur->next;}return sz;
}
LN* ListFind(LN* phead, Datatype x)
{assert(phead);LN* cur = phead->next;while (cur!=phead){if (cur->data == x)return cur;cur = cur->next;}return NULL;
}
void ListInsert(LN* pos, Datatype x)
{assert(pos);LN* newnode = BuyListNode(x);LN* posprev = pos->prev;newnode->next = pos;pos->prev = newnode;newnode->prev = posprev;posprev->next = newnode;
}
void PosErase(LN* pos)
{assert(pos);LN* posprev = pos->prev;LN* posnext = pos->next;free(pos);posprev->next = posnext;posnext->prev = posprev;
}
void DestoryList(LN* phead)
{assert(phead);LN* cur = phead->next;while (cur != phead){LN* next = cur->next;free(cur);cur = next;}free(phead);
}

 List.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int Datatype;
typedef struct ListNode
{int data;struct ListNode* next;struct ListNode* prev;
}LN;LN* BuyListNode(Datatype x);
LN* InItNode();
void PrintList(LN* phead);
void PushBack(LN* phead,Datatype x);
void PopBack(LN* phead);
void PushFront(LN* phead, Datatype x);
void PopFront(LN* phead);
LN* ListFind(LN* phead, Datatype x);
int Listsize(LN* phead);
void ListInsert(LN* pos, Datatype x);
void PosErase(LN* pos);
void DestoryList(LN* phead);

 test.c

#include"List.h"
void test1()
{LN* plist = InItNode();PushBack(plist, 1);PushBack(plist, 2);PushBack(plist, 3);PushBack(plist, 4);PushBack(plist, 5);PushFront(plist, 0);PopBack(plist);ListInsert(plist, 10);LN* pos = ListFind(plist, 10);ListInsert(pos, 11);PosErase(pos);PrintList(plist);DestoryList(plist);plist = NULL;
}
void test2()
{LN* plist = InItNode();PushBack(plist, 1);PushBack(plist, 2);PushBack(plist, 3);PushBack(plist, 4);PushBack(plist, 5);//PushFront(plist, 0);PopFront(plist);PrintList(plist);
}int main()
{test1();return 0;
}


 

总结

        带头双向循环链表作为一种数据结构,在解决问题时展现了其独特的优势。通过快速的插入和删除操作,以及灵活的双向遍历和循环遍历能力,它为我们提供了一种高效、灵活的数据组织方式。在实际应用中,我们可以充分发挥带头双向循环链表的特性,优化算法设计,提高程序的效率和可维护性。通过深入学习和掌握带头双向循环链表,我们可以更好地解决实际问题,提升自己的编程能力。希望本文能够帮助读者对带头双向循环链表有更深入的理解,并在实践中得到应用。最后,感谢阅读!

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

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

相关文章

IDEA的实用快捷键大全

目录 1.常规快捷键 1.1通用类 1.2注释类 1.3操作类 1.4展开与关闭 2.智能补全类快捷键 3.程序结构类快捷键 4.统一操作快捷键 1.常规快捷键 1.1通用类 像 Ctrl C 复制&#xff0c; Ctrl V 粘贴&#xff0c; Ctrl S保存文件&#xff0c; Ctrl X剪切&#xff0c;这种…

【深度学习】SMILEtrack: SiMIlarity LEarning for Multiple Object Tracking,论文

论文&#xff1a;https://arxiv.org/abs/2211.08824 代码&#xff1a;https://github.com/WWangYuHsiang/SMILEtrack 文章目录 AbstractIntroductionRelated WorkTracking-by-DetectionDetection methodData association method Tracking-by-Attention Methodology架构概述外观…

Excel功能总结

1&#xff09;每一张表格上都打印表头 “页面布局”-->“打印标题”-->页面设置“工作表”页-->打印标题“顶端标题行” 如&#xff1a;固定第1~2行&#xff0c;设置成“$1:$2” 2&#xff09;将页面内容打印在一页【缩印】 1.选好需要打印的区域&#xff0c;“页面布…

“单片机定时器:灵活计时与创新功能的关键“

学会定时器的使用对单片机来说非常重要&#xff0c;因为它可以帮助实现各种时序电路。时序电路在工业和家用电器的控制中有广泛的应用。 举个例子&#xff0c;我们可以利用单片机实现一个具有按钮控制的楼道灯开关。当按钮按下一次后&#xff0c;灯会亮起并持续3分钟&#xff…

删除这4个文件夹,流畅使用手机无忧

在现代社会中&#xff0c;手机已经成为我们生活中不可或缺的一部分。然而&#xff0c;随着使用时间的增长&#xff0c;我们可能会遇到手机卡顿和内存不足的问题&#xff0c;让我们感到十分困扰。手机卡顿不仅影响使用体验&#xff0c;还可能导致应用程序运行缓慢&#xff0c;甚…

网络安全 Day26-PHP 简单学习

PHP 简单学习 1. 为什么要学习PHP2. PHP语法3. php 变量4. 字符串数据5. PHP 函数6. 数组 1. 为什么要学习PHP php存量多开源软件多很多安全流程 渗透方法 sql注入基于PHP语言入门简单 2. PHP语法 格式: <?php 内容?>或<?内容?>结尾分号例子<?php phpin…

网络安全(秋招)如何拿到offer?(含面试题)

以下为网络安全各个方向涉及的面试题&#xff0c;星数越多代表问题出现的几率越大&#xff0c;祝各位都能找到满意的工作。 注&#xff1a;本套面试题&#xff0c;已整理成pdf文档&#xff0c;但内容还在持续更新中&#xff0c;因为无论如何都不可能覆盖所有的面试问题&#xf…

Python小白学习:超级详细的字典介绍(字典的定义、存储、修改、遍历元素和嵌套)

目录 一、字典简介1.1 创建字典1.2 访问字典中的值1.3 添加键值对1.4 修改字典中的值实例 1.5 删除键值对1.6 由多个类似对象组成的字典1.7 使用get()访问值1.8 练习题 二、遍历字典2.1 遍历所有键值对实例 2.2 遍历字典中的所有键2.3 按照特定顺序遍历字典中的所有键2.4 遍历字…

在java中存储对象到redis出现类型转换异常的解决方法

**出现的问题,**此时的redisCatch已经注入 原因:这里传进来的是一个对象,redis不能直接将对象存到String中,必须将对象进行序列化转成json字符串再存储,其次.传进来的对象不能是null 再重新启动就行了

Java阶段五Day21

Java阶段五Day21 文章目录 Java阶段五Day21问题解析rocketmq清空数据 linux学习背景什么是linux系统虚拟机介绍启动 虚拟机linux虚拟机网络的问题 linux系统的基础命令命令提示符命令格式pwd指令ls指令cd指令mkdirtouch指令cp指令rm指令mv指令cat指令tail指令 文本编辑器vim操作…

k8s存储卷

目录 一、为什么要存储卷&#xff1f;二、emptyDir存储卷三、hostPath存储卷四、 nfs共享存储卷五、PVC 和 PV5.1 PV和PVC之间的相互作用遵循的生命周期5.2 PV 的状态5.3 一个PV从创建到销毁的具体流程 六、静态创建pv和pvc资源由pod运用过程6.1 在NFS主机上创建共享目录&#…

Hopfield神经网络求解旅行商(TSP)问题matlab代码

1案例背景 1.1连续Hopfield神经网络概述 1.网络结构 连续Hopfield神经网络(Continuous Hopfield Neural Network,CHNN)的拓扑结构和离散Hopfield神经网络的结构类似,如图11-1所示。连续Hopfield网络和离散Hopfield 网络的不同点在于其传递函数不是阶跃函数,而是连续函…

《皮囊》阅读笔记

《皮囊》阅读笔记 2023年8月2号在杭州小屋读完&#xff0c;该书共收录14篇散文&#xff0c;内容大致分为两部分&#xff1a;前半部分讲述作者的阿太&#xff08;外婆的母亲&#xff09;、母亲、父亲关于生活哲学、房子、疾病与信仰的故事&#xff0c;后半部分讲述生活在小镇的张…

SpringBoot Plus+代码生产器

0目录 1. Mybatis Plus 2.代码生产器 1.Mybatis Plus 创建数据库和表&#xff08;id没有设置主键和自增长&#xff09; 创建springBoot导入依赖 安装lombok 配置yml 实体类加入注解 无参构造和有参构造 Mapper接口 扫描接口 测试 加入日志 添加 数据库…

JVM问题

1. jvm运行时区域划分及每个区域的作用 堆、方法区&#xff08;元空间&#xff09;、虚拟机栈、本地方法栈、程序计数器 2. 堆内存分配策略&#xff1a;新生代&#xff0c;老年代&#xff0c;gc时机 • 对象优先分配在Eden区&#xff0c;如果Eden区没有足够的空间进行分配时&am…

idea打开传统eclipse项目

打开传统web项目 1.打开后选择项目文件 2.选择项目结构 3.设置jdk版本 4.导入当前项目模块 5.选择eclipse 6. 设置保存目录 7.右键模块&#xff0c;添加spring和web文件 8. 设置web目录之类的&#xff0c;并且创建打包工具 9.如果有本地lib&#xff0c;添加为库 最后点击应用&…

深度学习里面为什么喜欢用对数

1.这样可以简化计算并提高稳定性&#xff0c;有着相同的临界点 2.由P(A).P(B)给出两个独立事件A和B共同出现的概率。如果我们使用log&#xff0c;即log(P(A)) log(P(B))&#xff0c;这很容易映射到一个和。因此&#xff0c;更容易将神经元触发的“事件”作为线性函数来处理。…

QT图形视图系统 - 使用一个项目来学习QT的图形视图框架 - 始篇

文章目录 QT图形视图系统介绍开始搭建MainWindow框架设置scene的属性缩放功能的添加加上标尺 QT图形视图系统 介绍 详细的介绍可以看QT的官方助手&#xff0c;那里面介绍的详细且明白&#xff0c;需要一定的英语基础&#xff0c;我这里直接使用一个开源项目来介绍QGraphicsVi…

LabVIEW深度相机与三维定位实战(下)

‍‍&#x1f3e1;博客主页&#xff1a; virobotics的CSDN博客&#xff1a;LabVIEW深度学习、人工智能博主 &#x1f384;所属专栏&#xff1a;『LabVIEW深度学习实战』 &#x1f37b;上期文章&#xff1a;『LabVIEW深度相机与三维定位实战&#xff08;上&#xff09;』 &#…

纯小白也能看懂,十分钟帮你快速了解云原生概念

纯小白也能看懂&#xff0c;十分钟帮你了解云原生技术 一、麻烦的一天二、魔法的种子1. Docker2. Kubernetes 三、渐入佳境1. 技术与术语容器化技术DevOps弹性伸缩Sidecar服务网格 2. 组件与框架DockerKubernetesHelmIstioPrometheusJaegerEnvoy 四、前路漫漫 随着云原生相关技…