数据结构入门篇 之 【双链表】的实现讲解(附完整实现代码及顺序表与线性表的优缺点对比)

在这里插入图片描述
一日读书一日功,一日不读十日空
书中自有颜如玉,书中自有黄金屋

一、双链表

1、双链表的结构

2、双链表的实现

1)、双向链表中节点的结构定义

2)、初始化函数 LTInit

3)、尾插函数 LTPushBack

4)、头插函数 LTPushFront

5)、尾删函数 LTPopBack

6)、头删函数 LTPopFront

7)、查找函数 LTFind

8)、在指定位置之后插入数据函数 LTInsert

9)、删除指定位置数据函数 LTErase

10)、销毁函数 LTDesTroy

二、双链表完整代码

三、顺序表和链表的优缺点对比

四、完结撒❀

前言

学习前先思考3个问题:

1.顺序表和链表的关系是什么?
2.链表的分类有哪些?
3.顺序表和链表的优缺点有哪些?

–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–❀–
1.顺序表和链表的关系是什么

我们之前学习了“顺序表”,“单链表”。
链表和顺序表都是线性表
线性表是指

逻辑结构:一定是线性的。
物理结构:不一定是线性的。

在这里插入图片描述物理结构是指表在内存中开辟的空间结构,顺序表的物理结构是连续的,而链表的物理结构是不连续的,但它们的逻辑结构都是连续的。

2.链表的分类有哪些?
链表根据带头或者不带头单向或者双向循环或者不循环一共分为8种
我们之前所学的单链表全名是叫:不带头单向不循环链表,而现在要学习的双链表是叫带头双向循环链表
双链表:
在这里插入图片描述掌握单链表和双链表对于其他链表的实现也就不那么困难了。

3.顺序表和链表的优缺点有哪些?
这里涉及到顺序表和链表的对比,先讲解双向链表,这放到博客末尾为大家对比讲解

一、双链表

1、双链表的结构

在这里插入图片描述注意:这里的“带头”跟前面我们说的“头节点”是两个概念。
带有节点里的头节点实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”

“哨兵位”存在的意义:
遍历循环链表避免出现死循环。

2、双链表的实现

对于双向链表的实现,我们依然使用List.h,List.c,test.c,三个文件进行实现。

1)、双向链表中节点的结构定义

上面我们简单介绍过双链表,其全名为:带头双向循环链表

带头:指链表中带有哨兵位
双向:双链表的每个节点内含有两个链表指针变量,分别指向前一个节点和后一个节点,所以就可以通过一个节点找到这个节点前后的两个节点。
循环:链表中的每个节点互相连接,最后一个节点与哨兵位相连构成一个环,整体逻辑结构可以进行循环操作

代码如下:

//定义双向链表中节点的结构
typedef int LTDataType;
typedef struct ListNode
{struct ListNode* prev;LTDataType data;struct ListNode* next;
}LTNode;

这里将结构体进行了重命名为LTNode。

2)、初始化函数 LTInit

在创建双链表中的哨兵位时我们需要对其进行初始化,防止意料之外的情况发生。
根据所传形参的类型不同,我们有两种写法
代码如下:

方案1

//方案1
void LTInit(LTNode** pphead)
{(*pphead) = (LTNode*)malloc(sizeof(LTNode));if (*pphead == NULL){perror("mallic:");exit(1);}(*pphead)->data = -1;(*pphead)->prev = (*pphead)->next = *pphead;
}

方案2

LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc:");exit(1);}newnode->data = x;newnode->next = newnode->prev = newnode;return newnode;
}//方案2
LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;
}

方案2里面包含了节点空间申请的函数,只是简单的创建双链表的节点,这里就不展开讲解了。

3)、尾插函数 LTPushBack

老规矩,我们开始实现管理链表数据的函数,这里讲的是头插。
在这里插入图片描述假如我们要在链表中尾插一个6,那么我们是需要先创建一个节点来存储6,下面分两步:

1.将6的节点里面前(prev)后(next)链表指针变量对应与原链表的尾节点d3和哨兵位head进行连接
2.将原链表尾节点d3的后链表指针变量(next)指向6的节点,再将哨兵位head的前链表指针变量(prev)指向6的节点

完成上面两部就实现了节点的插入。
代码如下:

//尾插
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;
}

这里的LTBuyNode函数在初始化函数中提到过。

4)、头插函数 LTPushFront

实现了尾插,头插也是大同小异。
头插是指在哨兵位后面的进行插入,第一个有效节点之前插入,即为头插。
在这里插入图片描述根据上图,进行头插

1.改变插入节点6的前后链表指针变量的指向。
2.再分别改变哨兵位head后链表指针变量(next)和第一个有效节点d1的前链表指针变量(prev)的指向。

代码如下:

//头插
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;
}

也是简简单单。

5)、尾删函数 LTPopBack

尾删函数的操作如下图:
在这里插入图片描述
1.改变哨兵位的前链表指针变量,指向原链表(还没尾删时的链表)尾节点d3的前链表指针变量(即倒数第二个节点d2的地址)。
2.相反,再将d2节点的后链表指针变量(next)指向哨兵位head。

代码如下:

//尾删
void LTPopBack(LTNode* phead)
{assert(phead);//链表只有一个哨兵位也不行assert(phead != phead->next);LTNode* del = phead->prev;//要进行尾删的节点LTNode* ddel = del->prev;//要进行删除的前一节点phead->prev = ddel;ddel->next = phead;free(del);del = NULL;
}

记得最后将尾删的节点空间进行释放。

6)、头删函数 LTPopFront

头删函数的操作如下图:
在这里插入图片描述与尾删也是大同小异
代码如下:

//头删 在哨兵位之后进行删除
void LTPopFront(LTNode* phead)
{assert(phead);assert(phead != phead->next);LTNode* del = phead->next;//要进行删除的节点LTNode* ddel = del->next;//要进行删除节点的下一个节点phead->next = ddel;ddel->prev = phead;free(del);del = NULL;
}

7)、查找函数 LTFind

既然要查找,那么肯定需要遍历链表并且也要保证链表不为空

在双链表中,当链表中只剩下哨兵位,那么这个链表即为空链表。

代码如下:

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);assert(phead != phead->next);//遍历链表LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}

断言时进行的assert(phead != phead->next);便是判断链表是否为空的条件。
而while (pcur != phead)是判断是否将链表遍历完的条件。
找到的话就返回给节点的地址,没有找到就返回空指针。

8)、在指定位置之后插入数据函数 LTInsert

操作过程如下图所示:
在这里插入图片描述假设我们在节点d2后面进行节点的插入,那么会受到影响的就是d2节点的后链表指针变量(next)和d3节点的前链表指针变量(prev),需要执行的操作:

1.将newnode节点的前链表指针变量(prev)指向d2节点,再将newnode节点的后链表指针变量(next)指向d3节点
2.将d3节点的前链表指针变量(prev)指向newnode’节点,再将d2节点的后链表指针变量(next)指向newnode节点。

注意! 第2步指针变量改变指向的先后顺序不能改变,不然指向地址不正确!

代码如下;

//删除pos位置的数据
void LTErase(LTNode* pos)
{assert(pos);LTNode* del = pos->next;//pos之后的数据LTNode* front = pos->prev;//pos之前的数据front->next = del;del->prev = front;free(pos);pos = NULL;
}

9)、删除指定位置数据函数 LTErase

操作过程如下图所示:
在这里插入图片描述假设删除d3节点,很明显这就是尾删操作,所以删除指定位置数据与其他删除函数也是一样的原理,其影响到的就是删除节点前后的节点链表指针的指向。

代码如下:

//删除pos位置的数据
void LTErase(LTNode* pos)
{assert(pos);LTNode* del = pos->next;//pos之后的数据LTNode* front = pos->prev;//pos之前的数据front->next = del;del->prev = front;free(pos);pos = NULL;
}

重要的是最在要记得将删除的节点空间进行销毁。

10)、销毁函数 LTDesTroy

那么最后的一个函数,销毁函数。
创建双建表使用后我们一定不要忘记进行销毁,将开辟的内存空间归还给计算机,不然在以后中可能会出现内存泄漏的工作事故。
销毁函数也根据传参类型不同有两种方案
代码如下:
方案1

//方案1
void LTDesTroy(LTNode* phead)
{assert(phead);assert(phead != phead->next);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}
}

方案2

//方案2
void LTDesTroy(LTNode** pphead)
{assert(pphead);assert(*pphead);LTNode* pcur = (*pphead)->next;while (pcur != (*pphead)){LTNode* next = pcur->next;free(pcur);pcur = next;}free((*pphead));(*pphead) = NULL;
}

对比之下方案1所传的形参为一级指针,而方案2为二级指针,因此我们是可以在方案2中直接对形参解引用得到双链表的哨兵位进行释放,而方案1并不行。
所以大家评判一下是那种方案更好呢?
其实是方案1更好,因为我们需要保持接口一致性,细心的同学可能已经发现了,之前所写的函数形参都为一级指针,所以我们在写代码的时候保持接口一致性也是很重要的,所以方案1更合适一些,至于链表中哨兵位的释放,我们下面在销毁函数外(主函数内)进行销毁即可。

二、双链表完整代码

List.h:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//定义双向链表中节点的结构
typedef int LTDataType;
typedef struct ListNode
{struct ListNode* prev;LTDataType data;struct ListNode* next;
}LTNode;//注意双向链表是带有哨兵位的,插入数据之前链表中必须先插入一个哨兵位//void LTInit(LTNode** pphead);
LTNode* LTInit();
void LTDesTroy();//尾插
void LTPushBack(LTNode* phead, LTDataType x);//头插
void LTPushFront(LTNode* phead, LTDataType x);//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);//查找
LTNode* LTFind(LTNode* phead,LTDataType x);//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置的数据
void LTErase(LTNode* pos);

List.c:

newnode->data = x;newnode->next = newnode->prev = newnode;return newnode;
}//方案1
//void LTInit(LTNode** pphead)
//{
//	(*pphead) = (LTNode*)malloc(sizeof(LTNode));
//	if (*pphead == NULL)
//	{
//		perror("mallic:");
//		exit(1);
//	}
//
//	(*pphead)->data = -1;
//	(*pphead)->prev = (*pphead)->next = *pphead;
//}//方案2
LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;
}//尾插
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;
}void LTPrint(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}//头插
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;
}//尾删
void LTPopBack(LTNode* phead)
{assert(phead);//链表只有一个哨兵位也不行assert(phead != phead->next);LTNode* del = phead->prev;//要进行尾删的节点LTNode* ddel = del->prev;//要进行删除的前一节点phead->prev = ddel;ddel->next = phead;free(del);del = NULL;
}//头删 在哨兵位之后进行删除
void LTPopFront(LTNode* phead)
{assert(phead);assert(phead != phead->next);LTNode* del = phead->next;//要进行删除的节点LTNode* ddel = del->next;//要进行删除节点的下一个节点phead->next = ddel;ddel->prev = phead;free(del);del = NULL;
}//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);assert(phead != phead->next);//遍历链表LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//删除pos位置的数据
void LTErase(LTNode* pos)
{assert(pos);LTNode* del = pos->next;//pos之后的数据LTNode* front = pos->prev;//pos之前的数据front->next = del;del->prev = front;free(pos);pos = NULL;
}//方案1
void LTDesTroy(LTNode* phead)
{assert(phead);assert(phead != phead->next);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}
}//方案2
//void LTDesTroy(LTNode** pphead)
//{
//	assert(pphead);
//	assert(*pphead);
//
//	LTNode* pcur = (*pphead)->next;
//	while (pcur != (*pphead))
//	{
//		LTNode* next = pcur->next;
//		free(pcur);
//		pcur = next;
//	}
//	free((*pphead));
//	(*pphead) = NULL;
//}

三、顺序表和链表的优缺点对比

学到这里大家会感觉双链表听起来可能比较复杂,但学完之后感觉比顺序表和单链表还容易,事实就是如此。
顺序表和双向链表优缺点分析:
在这里插入图片描述由上图,并不是双链表一定比顺序表好。
顺序表和双链表各有优势,我们在使用中要根据实际情况选择适合的线性表进行存储就是最好的。

四、完结撒❀

如果以上内容对你有帮助不妨点赞支持一下,以后还会分享更多计算机知识,我们一起进步。
最后我想讲的是,据说点赞的都能找到漂亮女朋友
在这里插入图片描述

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

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

相关文章

01-java入门了解--cmd命令、jdk、java的认识

cmd常用命令 java入门需要安装的环境 jdk。&#xff08;下载好jdk&#xff0c;并配置好环境&#xff09;idea。&#xff08;或者其他的编程工具&#xff09; jdk安装目录介绍 第一步&#xff1a;编写程序&#xff08;程序员写.java后缀的文件&#xff09; 第二步&#xff1a;…

学生时期学习资源同步-1 第一学期结业考试题1

原创作者&#xff1a;田超凡&#xff08;程序员田宝宝&#xff09; 版权所有&#xff0c;引用请注明原作者&#xff0c;严禁复制转载

使用CrossOver 在Mac 运行Windows 软件|D3DMetal是什么技术,

CrossOver Mac 使用特点 • 免费试用 14 天&#xff0c;可使用 CrossOver Mac 全部功能&#xff0c;• 试用过期会保留之前安装的 Windows 软件• 使 Mac 运行 Windows 程序 使用CrossOver在Mac上运行Windows软件是一个方便且无需安装完整Windows操作系统的解决方案。CrossOve…

数据仓库的基本概念、基本特征、体系结构

个人看书学习心得及日常复习思考记录&#xff0c;个人随笔。 数据仓库的基本概念、基本特征 数据仓库的定义&#xff1a;数据仓库是一个面向主题的、集成的、不可更新的、随时间不断变化的数据集合&#xff0c;用以更好地支持企业或组织的决策分析处理。 数据仓库中数据的4个…

(黑马出品_高级篇_02)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式

&#xff08;黑马出品_高级篇_02&#xff09;SpringCloudRabbitMQDockerRedis搜索分布式 微服务技术——分布式事务 今日目标1.分布式事务问题1.1.本地事务1.2.分布式事务1.3.演示分布式事务问题 2.理论基础2.1.CAP定理2.1.1.一致性2.1.2.可用性2…

力扣由浅至深 每日一题.05 合并两个有序列表

神明渡我&#xff0c;我将所有苦难都放过 —— 24.3.13 21. 合并两个有序链表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,…

K8s-CRD实战

CRD CRD的全称是CustomResourceDefinition,是Kubernetes为提高可扩展性, 让开发者去自定义资源&#xff08;如Deployment&#xff0c;StatefulSet等&#xff09;的一种方法. Controller controller是由controller-manager进行管理&#xff0c;通过API Server提供的接口实时监…

WWW2024 | PromptMM:Prompt-Tuning增强的知识蒸馏助力多模态推荐系统

论文&#xff1a;https://arxiv.org/html/2402.17188v1 代码&#xff1a;https://github.com/HKUDS/PromptMM 研究动机 多模态推荐系统极大的便利了人们的生活,比如亚马逊和Netflix都是基于多模态内容进行推荐的。对于研究,人们也遵循工业界的趋势,进行modality-aware的用户…

GUROBI之数学启发式算法Matheuristics

参考运小筹的帖子&#xff1a;优化求解器 | Gurobi 数学启发式算法&#xff1a;参数类型与案例实现 - 知乎 (zhihu.com) 简言之&#xff0c;数学启发式是算法就是数学规划和启发式算法的融合&#xff0c;与元启发式算法相比&#xff0c;数学启发式算法具有更强的理论性。 在GUR…

手写超级好用的rabbitmq-spring-boot-start启动器

手写超级好用的rabbitmq-spring-boot-start启动器 文章目录 1.前言2.工程目录结构3.主要实现原理3.1spring.factories配置3.2EnableZlfRabbitMq配置3.3RabbitAutoConfiguration配置3.4ZlfRabbitMqRegistrar配置 4.总结 1.前言 由于springBoot官方提供的默认的rabbitMq自动装配不…

计算机网络-第6章 应用层(2)

6.5 电子邮件 电子邮件&#xff0c;把邮件发送到收件人使用的邮件服务器&#xff0c;并放在其中的收件人邮箱中。最重要的两个标准&#xff1a;简单邮件传送协议SMTP&#xff0c;互联网文本报文格式。 SMTP只能传7位ASCII码邮件&#xff0c;93年提出互联网邮件扩充MIME。邮件…

【关注】国内外经典大模型(ChatGPT、LLaMA、Gemini、DALL·E、Midjourney、文心一言、千问等

以ChatGPT、LLaMA、Gemini、DALLE、Midjourney、Stable Diffusion、星火大模型、文心一言、千问为代表AI大语言模型带来了新一波人工智能浪潮&#xff0c;可以面向科研选题、思维导图、数据清洗、统计分析、高级编程、代码调试、算法学习、论文检索、写作、翻译、润色、文献辅助…