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

news2024/11/17 19:44:28

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

一、双链表

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.coloradmin.cn/o/1515234.html

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

相关文章

04_拖动文件渲染在页面中

新建一个文件夹&#xff0c;跟之前一样&#xff0c;在 Vscode 终端里输入 yarn create electron-app Drag。 在 index.html 添加以下代码&#xff0c;JS 文件夹和 render.js 都是新创建的&#xff1a; 首先&#xff0c;css 文件一般和 html 结合使用&#xff0c;相当于 html 是…

SQL-Labs靶场“32-33”关通关教程

君衍. 一、32关 GET单引号闭合宽字节注入1、源码分析2、宽字节注入原理3、联合查询注入4、updatexml报错注入5、floor报错注入 二、33关 GET单引号addslashes逃逸注入1、源码分析2、联合查询注入3、updatexml报错注入4、floor报错注入 一、32关 GET单引号闭合宽字节注入 请求方…

yolov5-模型蒸馏算法

一般来说模型剪枝之后精度都会下降,微调之后会恢复一部分,但仍然达不到剪枝前的精度,因此蒸馏会在微调阶段配合使用 蒸馏是一种基于“教师-学生网络”的训练方法,教师模型参数量较大,效果更好,学生模型参数量较少,效果较差,蒸馏即让小模型学习大模型的知识,提升小模型…

ADO .Net操作SQL Server数据库

//ADO.NET是.NET Framework提供的数据访问服务的类库&#xff0c;应用程序可以使用ADO.NET连接到这些数据源&#xff0c;并检索、处理和更新数据 //常用的数据源包括四种&#xff1a;&#xff08;1&#xff09;Microsoft SQL Server数据源&#xff1a;使用System.Data.SqlClien…

力扣27. 移除元素

思路&#xff1a;数组的空间是连续的&#xff0c;没办法删除&#xff0c;所以只能是覆盖&#xff1b; 把有用的元素排上来之后&#xff0c;剩下的空间放什么元素可以直接忽视&#xff0c;然 后我们只需要返回新数组中长度即可&#xff1b; 快慢指针法&#xff1a;我们需要新建两…

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

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

[LeetCode][LCR169]招式拆解 II——巧妙利用字母的固定顺序实现查找复杂度为O(1)的哈希表

题目 LCR 169. 招式拆解 II 某套连招动作记作仅由小写字母组成的序列 arr&#xff0c;其中 arr[i] 第 i 个招式的名字。请返回第一个只出现一次的招式名称&#xff0c;如不存在请返回空格。 示例 1&#xff1a; 输入&#xff1a;arr "abbccdeff" 输出&#xff1a;a…

基于SSM的协同过滤算法的电影推荐系统(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的协同过滤算法的电影推荐系统&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通…

LDA主题模型学习笔记

&#xff08;1&#xff09;LDA的基本介绍&#xff08;wiki&#xff09; LDA是一种典型的词袋模型&#xff0c;即它认为一篇文档是由一组词构成的一个集合&#xff0c;词与词之间没有顺序以及先后的关系。一篇文档可以包含多个主题&#xff0c;文档中每一个词都由其中的一个主题…

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

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

前端学习笔记 | WebAPIs(DOM+BOM)

一、作用和分类 1、基本概念 作用&#xff1a;使用JS去操作HTML和浏览器 分类&#xff1a;DOM&#xff08;文档对象模型&#xff09;和BOM&#xff08;浏览器对象模型&#xff09; html的标签JS的DOM对象 2、获取DOM对象-参数必须加引号 &#xff08;1&#xff09;选择匹配的第…

计算机三级错题整理

计算机三级整理 注意事项 第二道大题1.&#xff08;第二套&#xff09;2.&#xff08;第四套真题&#xff09;3.三十一套 第三道大题1.&#xff08;第一套真题&#xff09;2.&#xff08;第二份真题&#xff09;3.&#xff08;第四套真题&#xff09;4.二十九套5.三十套6.三十三…

《智能便利,畅享便利柜平台的架构奇妙之旅》

便利柜平台作为一种智能化、便捷的自助服务解决方案&#xff0c;正在逐渐走进人们的生活。本篇博客将深入探讨便利柜平台的架构设计理念、优势和实践&#xff0c;帮助读者了解如何构建智能便利柜平台&#xff0c;提供更便捷的自助服务体验。 ### 便利柜平台架构设计 #### 1. …

UE4案例记录

UE4案例记录&#xff08;制作3D角色显示在UI中&#xff09; 制作3D角色显示在UI中 转载自youtube视频 https://www.youtube.com/channel/UCC8f6SxKJElVvaRb7nF4Axg 新建项目 创建一个Actor 场景组件->摄像机组件->场景捕获组件2D&#xff0c;之后添加一个骨骼网格体…

Python基础课堂最后一课23——正则对象

文章目录 前言一、正则对象是什么&#xff1f;二、正则表达式基本分类1.普通字符2.元字符 总结 前言 很开心能和你们一些学习进步&#xff0c;在这一个多月的时间中&#xff0c;是你们让我坚持了下来&#xff0c;完成了python基础课堂编写&#xff0c;不管如何&#xff0c;我们…

ubuntu 23.04 安装 中文输入法

1、安装 fcitx sudo apt install fcitxfcitx 安装好后&#xff0c;可以使用 fcitx-configtool 命令进行配置&#xff0c;其界面如下所示。在这里可以配置不同输入法的切换快捷键&#xff0c;默认输入法等。刚安装系统后&#xff0c;这里只有一个输入法&#xff0c;所以接下来要…

SORA和大语言模型的区别

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

HttpContext请求接收上下文模块设计与实现(http模块四)

目录 类功能 类定义 类实现 编译测试 类功能 类定义 // HttpContext接收请求上下文模块功能设计 typedef enum {RECV_HTTP_ERROR,RECV_HTTP_LINE,RECV_HTTP_HEAD,RECV_HTTP_BODY,RECV_HTTP_OVER } HttpRecvStatu;class HttpContext { private:int _resp_statu; …

刷题日记——反转公约数、循环位移(厦门大学机试)

题目 分析 将输入的数字看作字符串&#xff0c;然后将字符串转成真实值计算两个真实值&#xff0c;然后从1开始遍历公约数&#xff0c;每次发现一个更大的公约数就替换&#xff0c;直到找不到公约数 代码 #include <cstdio> #include <map> #include <string…

Redis第8讲——Cluster集群模式详解

前面两篇文章介绍了Redis主从和哨兵模式&#xff0c;不难发现&#xff0c;它们都有一些共同的缺点&#xff0c;首先在主从切换的过程中会丢失数据&#xff1b;另一个就是只有一个master&#xff0c;只能单点写&#xff0c;并没有水平扩容能力。而且每个节点都保存了所有的数据&…