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

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

一、双链表

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.mzph.cn/news/743371.shtml

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

相关文章

单据分页的实现

单据分页的实现 1. AceWzcgfkjtMaintainProxy.java package nc.ui.jych.wzcgfkjt.ace.serviceproxy;import nc.bs.framework.common.NCLocator; import nc.itf.jych.IWzcgfkjtMaintain; import nc.ui.uif2.components.pagination.IPaginationQueryService; import nc.vo.jych.…

软考高级:信息系统开发方法2(形式化方法、统计过程方法等)概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

RTC协议与算法基础 - RTP/RTCP

首先&#xff0c;需要说明下&#xff0c;webrtc的核心音视频传输是通过RTP/RTCP协议实现的&#xff0c;源码位于src/modules/rtp_rtcp目录下&#xff1a; 下面让我们对相关的内容基础进行简要分析与说明&#xff1a; 一、TCP与UDP协议 1.1、TCP协议 TCP为了实现数据传输的可…

【Python】新手入门学习:详细介绍依赖倒置原则(DIP)及其作用、代码示例

【Python】新手入门学习&#xff1a;详细介绍依赖倒置原则&#xff08;DIP&#xff09;及其作用、代码示例 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、Py…

如何将.txtpb在IDE中彩色高亮显示

1. 问题描述 文件内容片段如下&#xff0c;它采用了一种键值对的格式&#xff0c;其中还包括了注释。我们可以采用一种近似的语言色彩识别方案处理它&#xff0c;比如YAML或者Python的语法高亮规则&#xff0c;因为这两种语言在处理键值对和注释的表示上与内容片段相似。当然也…

【QT+QGIS跨平台编译】之七十三:【QGIS_Analysis跨平台编译】—【错误处理:字符串错误】

文章目录 一、字符串错误二、处理方法三、涉及到的文件一、字符串错误 常量中有换行符错误:(也有const char * 到 LPCWSTR 转换的错误) 二、处理方法 需要把对应的文档用记事本打开,另存为 “带有BOM的UTF-8” 三、涉及到的文件 涉及到的文件有: src\analysis\processin…

openssl3.2 - exp - 选择最好的内建椭圆曲线

文章目录 openssl3.2 - exp - 选择最好的内建椭圆曲线概述笔记将 openssl ecparam -list_curves 实现迁移到自己的demo工程备注END openssl3.2 - exp - 选择最好的内建椭圆曲线 概述 在openssl中使用椭圆曲线, 只允许选择椭圆曲线的名字, 无法给定椭圆曲线的位数. 估计每种椭…

深度学习armv8/armv9 cache的原理

文章目录 前言1、为什么要用cache?2、背景:架构的变化?2、cache的层级关系 ––big.LITTLE架构&#xff08;A53为例)3、cache的层级关系 –-- DynamIQ架构&#xff08;A76为例)4、DSU / L3 cache5、L1/L2/L3 cache都是多大呢6、cache相关的术语介绍7、cache的分配策略(alocat…

C++函数 加括号与不加括号

很多时候&#xff0c;我们会看到一些在创建对象时有的加括号有的不加括号 那么&#xff0c;这是什么情况呢&#xff1f; 总结&#xff1a;函数需要加上括号&#xff0c;加上括号会对函数初始化&#xff0c;不加括号可能导致未知错误 我们来验证一下。 1.基本数据类型不带括…

利用Python进行网络爬虫:Beautiful Soup和Requests的应用【第131篇—Beautiful Soup】

利用Python进行网络爬虫&#xff1a;Beautiful Soup和Requests的应用 在网络数据变得日益丰富和重要的今天&#xff0c;网络爬虫成为了获取和分析数据的重要工具之一。Python作为一种强大而灵活的编程语言&#xff0c;在网络爬虫领域也拥有广泛的应用。本文将介绍如何使用Pyth…

Elasticseach基础认识

ES的起源&#xff1f; Elasticsearch 是由 Elastic 公司创建 简称&#xff08;ES&#xff09; Elasticsearch 是一个分布式、免费和开放的搜索和分析引擎&#xff0c;适用于所有类型的数据&#xff0c;包括文本、数字、地理空间、结构化和非结构化数据。 Elasticsearch 基于 …

Oracle 主从切换脚本

一、 切换前预检查 1. dg_precheck_main_v1.4.sh #!/bin/bash#********************************************************************************** # Author: Hehuyi_In # Date: 2022年06月16日 # FileName: dg_precheck_main_v1.4.sh # # For sys user, execute the sc…

LLM之RAG实战(二十九)| 探索RAG PDF解析

对于RAG来说&#xff0c;从文档中提取信息是一种不可避免的场景&#xff0c;确保从源文件中提取出有效的内容对于提高最终输出的质量至关重要。 文件解析过程在RAG中的位置如图1所示&#xff1a; 在实际工作中&#xff0c;非结构化数据比结构化数据丰富得多。如果这些海量数据无…

如何使用vue定义组件之——子组件调用父组件数据

1.定义父子模板template <div class"container"><my-father></my-father><my-father></my-father><my-father></my-father><!-- 此处无法调用子组件&#xff0c;子组件必须依赖于父组件进行展示 --><!-- <my-…

数学实验_Matlab使用2_简单绘图

简单使用 x -pi * 2 : .1 : pi*2;y sin(x);plot(x, y); % 绘制普通图像plot(x, y, k-.*); % 绘制2维图像&#xff0c;线为实线&#xff0c;*为每个点&#xff08;Matlab的画图比较原始&#xff0c;就是简单的秒点画图&#xff09;grid on; % 打开网状格式% grid off; % 关闭…

SORA和大语言模型的区别

OpenAI的文生视频模型SORA与大语言模型&#xff08;LLM&#xff09;的主要区别在于它们的应用领域和处理的数据类型&#xff0c;数据处理能力、技术架构、多模态能力和创新点。SORA作为一款专注于视频生成的模型&#xff0c;展现了在处理视觉数据方面的独特优势和创新能力。 1…

R语言读取大型NetCDF文件

失踪人口回归&#xff0c;本篇来介绍下R语言读取大型NetCDF文件的一些实践。 1 NetCDF数据简介 先给一段Wiki上关于NetCDF的定义。 NetCDF (Network Common Data Form) is a set of software libraries and self-describing, machine-independent data formats that support…

STM32串口通信—串口的接收和发送详解

目录 前言&#xff1a; STM32串口通信基础知识&#xff1a; 1&#xff0c;STM32里的串口通信 2&#xff0c;串口的发送和接收 串口发送&#xff1a; 串口接收&#xff1a; 串口在STM32中的配置&#xff1a; 1. RCC开启USART、串口TX/RX所对应的GPIO口 2. 初始化GPIO口 …

YOLOv8改进 | 图像去雾 | 特征融合注意网络FFA-Net增强YOLOv8对于模糊图片检测能力(北大和北航联合提出)

一、本文介绍 本文给大家带来的改进机制是由北大和北航联合提出的FFA-net: Feature Fusion Attention Network for Single Image Dehazing图像增强去雾网络,该网络的主要思想是利用特征融合注意力网络(Feature Fusion Attention Network)直接恢复无雾图像,FFA-Net通过特征…

MyBatis-Plus学习记录

目录 MyBatis-Plus快速入门 简介 快速入门 MyBatis-Plus核心功能 基于Mapper接口 CRUD 对比mybatis和mybatis-plus&#xff1a; CRUD方法介绍&#xff1a; 基于Service接口 CRUD 对比Mapper接口CRUD区别&#xff1a; 为什么要加强service层&#xff1a; 使用方式 CR…