双向链表复习(C语言版)

目录

链表分类:

双向链表初始化:

双向链表的插入:

双向链表的打印:

双向链表的删除:

双向链表的指定结点位置查找:

双向链表的在指定位置之后插入数据:

注意:通过上文的指定结点位置查找最后返回来的结点,可以作为指定位置后数据插入函数的pos结点(具体如下图所示)

双向链表的删除指定位置结点:

双向链表的销毁:

什么是接口一致性?


 

链表分类:

        实际在链表领域,共有8种不同的链表。单链表的全称应该是不带头单向不循环链表,而本文讲解的双向链表全称为带头双向循环链表

        带头/不带头:含义为链表是否带有头结点。头结点只是为了后续操作的便捷,在对链表进行任意操作时,都不会发生变化。

        单向/双向:结点与结点之间的关系是单向的还是双向的。

        循环/不循环:循环链表意为链表的尾元结点和首元结点或头结点相连,不循环链表就没有这样的特点。

        以上三个链表可能存在的特点,通过排列组合(死去的记忆又回来啦!!!),  2 × 2 × 2 = 8,因此能够得到链表可以有8种不同类型。

f414edaef2134a82b28993ff3d3b8835.jpg

 aa9343ed87dd401492beab054f7fa50e.jpg

 

 

双向链表初始化:

 

a4ac79fdc7fa4f75810f2557061776a3.jpg

双向链表示意图

买一台电脑,初始化电脑,搞一个自己爱用的输入法,下载vs2022,在vs里创建3个小小的文件。一个测试、一个写功能实现、一个声明功能实现的函数还有头文件。

初始化方法:创建一个新链表,放一个头结点进去。那是不是我们时常得使用到新节点的开辟?

嗯🤔,那就搞一个函数专门来开辟新节点吧!

LTNode* LTBuyNode(LTDataType x)
{LTNode* node = (LTNode*)malloc(sizeof(LTNode));if (node == NULL){perror("malloc fail!");exit(1);}node->data = x;node->prev = node->next = node;return node;
}

  爱学习的小明同学看到这串代码,突发疑问。唉?不对啊!这不对啊!为什么代码会是

    node->prev = node->next = node;

这样来写呢?!我的NULL呢?!

答曰:单链表由于是不循环的,所以尾元结点的next应该指向NULL;双向链表是一个循环链表,所以在开辟新节点时,一定要让其自己指向自己。

然后,写个初始化函数,完成初始化。这里传二级指针是因为,创建新链表头结点是需要对传来的参数有所改变的。如对一级二级指针有所疑惑,可以试着看看笔者的这篇文章:单链表复习 (C语言版)。

8b0db6d1d0fe491d908a059097fdcaff.png

鉴于这篇文章,它又臭又长,笔者总结了一级二级指针的精华所在。

f2e32131f01c470e8a9ee2d9fa153b98.png

上面的解引用操作,讲的就是 * 操作符。如果还有疑惑,评论区见!(😀) 我是怎么用10149字写了两行字的内容的?

void LTInit(LTNode** pphead)
{*pphead = LTBuyNode(-1);//*pphead的data数据无需改变同时也不会使用,所以随便给个值即可
}

完结撒花!!!(bushi)

双向链表的插入:

双向链表的插入要分为头插、尾插两种。

尾插,顾名思义,就是在链表末尾后面插入;头插,顾名思义,就是在链表表头插入。我家门前两棵树,猜猜两棵什么树(doge)

但此处请注意!!!!!!(看我打了那么多感叹号,知道重要了吧),无论是头插还是尾插,都是不算头结点的。打个比方,头插是先从头结点的后面开始插入,尾插也一样;然后,每一次头插都是在插入好的结点前插入新结点;尾插类比一下,就是在插入好的结点前后插入新结点(cv大法好)。如下图所示:

b9cd684773d34a508a20c8c3f5d7869d.jpg afb3102b8b5643339349a1b3c7380602.jpg

头插

6c7b3b52f4cb4d0da3f7d2bc453abc9d.jpg 0b35d166e7434abfa539279bc38628e5.jpg

尾插

 

理清一些注意点以后,相信大家已经急不可待,想要上手啦。教练,我想敲代码!(超大声)

我说停停,让69岁的老同志再说两句。这边的插入,我们传给函数的是一级指针还是二级指针?

答曰:一级指针。在确定函数的形参到底是几级指针时,先得清楚传给函数的到底是什么内容。因为是插入,所以必须有一个具体类型的数据,作为结点的data值;同时,传给插入函数的应该是链表的头结点,头结点不能被改变!!!!!!所以,要传一级指针来确保头结点不被改变。

头插代码:

void LTPushFront(LTNode* phead,LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}

一.链表只有一个结点的情况(不算头结点):

  1. 开创一个新结点(新节点的next、prev开辟成功后都先指向自己)
  2. 让头结点的next、prev(原本都指向自己)都指向新节点
  3. 让新节点的next、prev都指向头结点
  4. 注意:newnode->next 一开始表示的即为 newnode,因为新节点的next初始化时是指向自己的,其他的可以此类推,不难得到:四行对链表进行操作的代码,赋值号右边都是结点自身。phead->next->prev拆解开来理解,即phead自身的prev;phead->next为phead的next

所以,可以看出上文代码也能满足只有一个结点的情况。 

 

二.链表有两个及两个以上结点的情况(不算头结点):

0dca589fbe5a42c7ab1fef6d40dedb2b.jpg

  1. 开创一个新结点
  2. 让新节点的next指向d1,prev指向头结点
  3. 让d1的prev指向新节点
  4. 让头结点的next指向新节点
  5. 注意:第3、第4步的顺序不能颠倒。如若颠倒,那么头结点的next就不是d1了,d1找不见了,d1的prev指向新节点就会出现问题。(当然,也可以从新结点出发,newnode的next依旧是d1)

 

 尾插代码:

void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}

        从上述代码,不难发现:只有一个节点时,newnode的prev指向头结点,next指向头结点;头结点的next指向新结点,prev指向新结点。满足要求。

        对于两个及两个以上结点,如下图所示。尾插结束以后,双向链表的环形结构不能被破坏,所以还需要对头结点prev以及尾元结点的next进行一定的操作(尾的next指向头,头的prev指向尾)。d3和头结点的关系断开,改为和新结点的双向关系。 

54b272f44388418cbba41d012757945b.jpg

双向链表的打印:

完成了双向链表的插入,得要开始进行测试。如果把所有的代码都写完,然后再调试,我们就会看到: 

b168a65102fc40dc934bde16be4757a4.png

一片欣欣向荣,那种勃勃生机万物竞发的境界犹在眼前! 

如何测试?

可以通过调试,也可以直接尝试打印。笔者此处选择了后者。

双向链表打印代码:
 

void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}

     注意点:

  1. 打印从头结点的下一个结点开始(形参即为头结点)
  2. 打印的循环退出条件不能是 == NULL,循环链表不会有为空的情况,需改为指向头结点的时候。因为尾元结点(需要打印的最后一个结点)的next为头结点,如下图所示:

827d6a9b4eb4411ba4c4ca0cc161e946.jpg

进行声明,在测试文件中尝试执行。上文代码都是在List.c文件中写的。

3be29a2592c04fbaaee69f287bbe82a8.png

58c9625271b449e5bfcce527aad3e79b.png

 

双向链表的删除:

删除分为了头删、尾删,你是什么山?

73a3f041c61e49dba062f3790ffa89d1.jpg

通过上图我们不难发现,尾删代码中会使用到 phead->prev->prev->next 这条语句,即链表的倒是第二个节点的next。这样一条语句又臭又长!改一改。

然后聪明的小王同学拍了拍脑袋就给了好办法,在尾删代码的开始处,直接定义一个 del =  phead->next ,del恰好是需要删除的结点,很长的代码也可以简化了。(让我们一起谢谢小王同学)

del被删除以后,尾元结点变成了del->prev,因此要让 del->prev 指向头结点,然后再让头结点指向 del->prev。

尾删代码:

void LTPopBack(LTNode* phead)
{assert(phead && phead != phead->next);LTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}

    注意点:

  1. 对于del的free置空操作必须放在最后,要不然就会对空指针解引用
  2. assert断言要限制的不只是头结点不能为空,同时链表存放数据的结点也不能一个没有。可以理解成只有头结点时,链表只是完成了初始化,但链表依然为空链表。

 写完尾删代码以后,依旧是声明尾删函数,测试尾删函数。(双向链表打印部分有讲解)

 

头删代码就是把尾删代码中,除断言语句外,其他语句中所有的next改为prev,所有的prev改为next。(聪明的你肯定也发现了,头插和尾插之间也存在这种关系)

void LTPopFront(LTNode* phead)
{assert(phead && phead->next != phead);LTNode* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}

 

双向链表的指定结点位置查找:

给该函数一个数据值,让函数在某个链表里寻找是否存在存放这个数据的结点,返回找到的那个节点。

LTNode* LTFind(LTNode* phead,LTDataType x)
{LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}

该函数的撰写思路类似于双向链表的打印,可以通过上文的打印部分来理解该函数的实现。

 

双向链表的在指定位置之后插入数据:

在指定位置后插入结点,一共分为两种情况。

  • 在结点与结点之间插入:这种插入方式就是让新结点的next指向指定位置的下一个结点,prev指向指定位置;让指定位置的下一个结点的prev指向新结点,让指定位置的next指向新结点。是不是绕晕了?(doge)那请看下图!

20375ae564d24f248f913d92998b1257.jpg

  • 尾插:请注意,请注意,请注意!此处的尾插代码不能直接用尾插函数。这是因为在指定位置后插入数据的函数,它的形参是指定位置的结点;而尾插函数(在插入部分有讲解),它的形参是头结点。尾插的具体方式如下图所示。 

bd2e9c16afa848e28ad053d3f39fb5ef.jpg

 

 唉?!巧了,太巧了!两种情况的代码又重合上了。是的,双向链表极限情况和一般情况的代码重合率真的很高,我永远喜欢双向链表!单链表什么货色啊?是能来碰瓷双向链表的?(仅代表作者观点,如有冒犯到单链表厨,在此深感抱歉)

指定位置后插入数据的代码:

void LTInsert(LTNode* pos,LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}

注意:通过上文的指定结点位置查找最后返回来的结点,可以作为指定位置后数据插入函数的pos结点(具体如下图所示)

4fa4046a8d744b6bbb64b3bab8c929c4.png

双向链表的删除指定位置结点:

写在前面:该函数的形参pos,对应的实参依旧是上图中所提到的find指针。

59a07ad826344ce9bee4b9f0a46bbb4c.jpg

删除完pos结点,让pos的前一个结点指向头结点,让头结点指向pos的前一个结点,然后再把使用完的find free置空 。如此即可,代码如下所示:

void LTErase(LTNode* pos)
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);
}

嗯哼?!是不是少了什么东西?!pos = NULL呢?

是的,没有错。在这个函数里,并没有这条语句,这究竟是人性的扭曲,还是道德的沦丧?

答曰:都不是!因为pos是个一级指针,所以在函数内将pos置为空后,实参find不会发生改变,所以写了也起不到效果。

解决办法:不在函数里置空,在函数外置空。具体如下图所示。

e45d479fdcca4755918a9e57a205c0c4.png

 

双向链表的销毁:
 

双向链表的销毁会涉及到链表的所有结点,头结点也需要被free置空。而销毁函数的形式参数,为保持接口一致性(上一个函数传一级指针也是为了这个),最好也是头结点的一级指针形式。同时,依旧是在函数外,对实参(头结点)进行置空操作。

因为需要删除所有结点,所以需要用到循环语句,然后退出条件为pcur(一开始指向phead->next)指向头结点。

在循环语句中,因为直接删除pcur结点,pcur的后续链表结点就找不到了。所以需要先搞一个next指针,将pcur结点的下一个结点储存起来,然后删除pcur对应的结点,如下图所示。

88040ab6d64645e3a8983f52e74ac6d7.jpg

删除完以后,让pcur、next都往后走一个结点,如下图所示。重复上述操作,销毁完成。

20ba388504844be2b82956cea9e39bbb.jpg

销毁代码:

void LTDesTory(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = NULL;pcur = next;}free(phead);
}// phead = NULL; 这条语句写在函数外

 

什么是接口一致性?

5361331cb1d9437180661e464453e1c1.png

上述代码中,出现了大量函数,都有一个规律:即除了初始化函数外,传输的都是一级指针。这就是接口一致性。

在项目的实际开发过程中,一会传个一级指针,一会传个二级指针,会增加代码的出错率,所以为了降低这个问题的出现概率,接口一致性一定是越高越好的。(当然,迫不得已的时候还是可以不管这个问题的。毕竟代码和我,有一个能跑就行) 

下班啦,下班啦!总算写完啦!

 

 

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

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

相关文章

地理科学专业| 中国大学排行榜(2024年)

地理科学专业| 中国大学排行榜(2024年)

客车制造5G智能工厂工业物联数字孪生平台,推进制造业数字化转型

制造业正经历着前所未有的变革,其中客车制造行业作为传统制造业的重要组成部分,正积极拥抱5G、工业物联网及数字孪生等先进技术,推动生产模式的全面升级与数字化转型。 客车制造5G智能工厂工业物联数字孪生平台的出现,不仅为行业…

【Linux】系列入门摘抄笔记-8-权限管理chmod/chown

Linux操作系统中文件的基本权限由9个字符组成,分别为属主、属组和其他用户,用于规定是否对文件有读、写和执行权限。 文件/目录的权限与归属 目录列表中,有9列 第一列:文件类型与权限(共10个字符,分为四组…

电子木鱼+提肛+游戏地图,车机还能这么玩?

文/王俣祺 导语:电子木鱼、提肛训练、游戏级地图,你很难想象这些“直男关怀”是来自小鹏MONA M03的车机系统。最近,一批关于MONA M03车机功能的视频在网上疯传,一系列“没用但有趣”的功能广受年轻用户的好评,情绪价值…

linux上用anaconda创建一个新环境,并将nicegui的应用打包为一个可执行应用

先下载好anaconda linux版本 Download Anaconda Distribution | Anacondahttps://www.anaconda.com/download/之后运行 conda create --name py311 python3.11 --name py311 是环境名 python3.11 是python版本 安装完成后,运行 conda env list 得到 这时我们…

手机使用技巧:如何恢复Android手机不见的短信

在您的 Android 手机上丢失短信可能是一种令人沮丧的经历,尤其是在文本包含重要信息的情况下。幸运的是,有一些方法可以在Android上恢复已删除的短信。在这篇博文中,我们将讨论几种在Android手机上恢复已删除短信的方法。 为什么需要恢复Andr…

【python】逐步回归(多元线性回归模型中的应用)

文章目录 前言一、逐步回归1. 前进法(Forward Selection)2. 后退法(Backward Elimination)3. 逐步回归法(Stepwise Regression) 二、示例三、代码实现----python 前言 Matlab中逐步回归的实现可以使用 Mat…

软体水枪在灭火工作中发挥什么作用_鼎跃安全

火灾,这一频繁侵袭我们日常生活的灾难性事件,以其迅猛之势对人类的生存环境与日常生活构成了极其严重的破坏与威胁。它不仅能够在瞬间吞噬财产,更可怕的是,它无情地剥夺了生命,破坏了家庭,给社会留下了难以…

关于Ubuntu中使用命令行安装Qt的一些分享

以Ubuntu 22.04为例。 1、安装默认的Qt库 sudo apt-get install qtbase5-dev qtbase5-dev-tools qtchooser 这条指令执行完会出现 usr/lib/x86_64-linux-gnu/qt5 文件,并伴随5个子文件夹,结构如下: 并且会出现 usr/lib/qt5, usr/lib/x86_6…

第5节:Elasticsearch核心概念

我的后端学习笔记大纲 我的ElasticSearch学习大纲 1.Lucene和Elasticsearch的关系: 1.Lucene:最先进、功能最强大的搜索库,直接基于lucene开发,非常复杂,api复杂2.Elasticsearch:基于lucene,封装了许多luc…

SpringBoot的自动配置原理探究

目录 什么是SpringBoot的自动配置(Auto-Configuration) 举例:SpringBoot自动配置(Redis的自动配置)的实例: 步骤1.:引入Redis启动器pom依赖 步骤2.在application.yml或者(proper…

XXL-JOB漏洞分析与利用

一、前言 在当今的数字化时代,任务调度平台对于企业级应用来说至关重要。它们负责自动化和协调各种时间敏感或周期性的任务,确保业务流程的顺畅运行。XXL-JOB作为一款流行的分布式任务调度平台,因其强大的功能和易用性,被广泛部署…

vue3父子组件双向数据绑定v-model;父组件调用子组件事件

效果&#xff1a; 父far.vue <template><div><div>父组件内容<pre>value1:{{ value1 }}</pre><el-button type"primary">flag1:{{ flag1 }}</el-button><pre>obj1:{{ obj1 }}</pre><el-input v-model&q…

进阶SpringBoot之 JDBC 篇

对于数据访问层&#xff0c;无论是SQL&#xff08;关系型数据库&#xff09;还是NOSQL&#xff08;非关系型数据库&#xff09;&#xff0c; Spring Boot 底层都是采用 Spring Data 的方式进行统一处理 创建一个新项目&#xff0c;依赖勾选 JDBC API、MySQL Driver 项目创建好…

2024.8.20 作业

目录 思维导图&#xff1a; 面试题练习&#xff1a; 1、C语言中指针数组和数组指针的区别 2、结构体字节对齐的原理 3、TCP和UDP的区别 4、同步通信和异步通信的区别 5、多线程的理解 6、大小端验证 7、互斥锁 8、共享内存特点 9、C语言的指针 10、gcc编译 11、socket套接字 1…

【TCP】确认应答、超时重传机制和TCP报头

TCP 相关机制 TCP 基本特点&#xff1a;有连接、可靠传输、面向字节流、全双工 有连接、面向字节流和全双工都能在前面的代码中体现有连接&#xff1a;必须要先调用 accept 建立联系才能处理面向字节流&#xff1a;会拿到 clientSocket 对象的 InputStream 和 OutputStream&a…

加密请求包的爆破

本文来源无问社区&#xff0c;更多实战内容可前往查看http://wwlib.cn/index.php/artread/artid/10414.html 在平时进行漏洞挖掘的时候经常会在诸如登陆的地方遇到密码经过了加密&#xff0c;而且不是也 base64 或者 md5 啥的&#xff0c;而可能是 RSA 之类的&#xff0c;这就…

Python 办公自动化 案例 将Excel 数据导入数据库 【2】推荐

前言&#xff1a; 前面我们梳理了如何处理Excel数据&#xff0c;详细的回顾了如何读取Excel行、列以及单元格数据&#xff0c;如何创建一个Excel、向Excel填充数据以及保存Excel数据。主要是xlrd读取和xlwt写入两个python第三方模块对Excel数据操作的一些常用函数以及属性。点…

【JVM】深入理解类加载机制(一)

深入理解类加载机制 Klass模型 Java的每个类&#xff0c;在JVM中都有一个对应的Klass类实例与之对应&#xff0c;存储类的元信息如:常量池、属性信息、方法信息…从继承关系上也能看出来&#xff0c;类的元信息是存储在元空间的。普通的Java类在JVM中对应的是InstanceKlass(C)…

4款AI 生成 PPT的工具,帮你赶上演示文稿的新趋势!

AI 生成 PPT 最大的优势就在于它能够帮助我们提高效率。如果我们自己制作的话就需要花费大量的时间去收集资料、构思布局、设计排版。而现在&#xff0c;有了AI工具&#xff0c;一切就迎刃而解&#xff0c;如果大家需要这样的工具&#xff0c;可以看看这4款。 1、笔灵办公 直通…