数据结构---链表(2)---双向链表

news2025/1/22 12:33:48

链表(1)中讲过了在OJ题中出现很多并且能作为一些复杂数据结构子结构的不带头单向不循环链表,下面讲解应用很广很实用的带头双向循环链表。

三、双向链表---DoublyLinkedList

演示带头双向循环链表(实用)。

带头--->不需要对空链表继续单独判断;循环--->可以很轻松的找到尾结点。 

集各种优势于一身的链表。

1.双向链表的结构

//双向带头循环链表
#define DLListDataType int 
typedef struct DLListNode
{
	struct DLListNode* prev;//前驱指针
	struct DLListNode* next;//后继指针
	DLListDataType data;//数据
}DLListNode;

链表是双向的,结构体成员不仅要有next指针用于指向下一个结点,同时需要有prev指针指向前一个结点。

2.双向链表的接口函数

①初始化DLListInit

void DLListInit ( DLListNode** pphead ) ;

对于带头的双向循环链表而言,我们不仅需要创建结构体指针变量,同时还需要创建一个哨兵位,我们在函数DLListInit中创建哨兵位,由于我们是对结构体指针进行修改,如果传入函数,需要传入指针的地址,函数使用结构体二级指针DLListNode** pphead接收才能够修改所创建的指针变量pdll的值:

DLListNode* pdll = NULL;
DLListInit(&pdll);
void DLListInit(DLListNode** pphead)
{
    assert(pphead);
    *pphead = (DLListNode*)malloc(sizeof(DLListNode));
	if (*pphead == NULL)
	{
		perror("malloc fail");
	}
	*pphead->next = *pphead;
	*pphead->prev = *pphead;
	*pphead->data = -1;
}

但是,如果我们不想用二级指针接收,也有办法,我们可以就用结构体指针接收,最后传回这个哨兵位的地址给指针变量pdll即可:

DLListNode* DLListInit ( ); 

DLListNode* pdll = DLListInit();
DLListNode* DLListInit()
{
	DLListNode* phead = (DLListNode*)malloc(sizeof(DLListNode));
	if (phead == NULL)
	{
		perror("malloc fail");
	}
	phead->next = phead;
	phead->prev = phead;
	phead->data = -1;
	return phead;
}

以上两种方法都能够达到目的。

注意,由于是循环链表,因此,我们将哨兵位的next和prev都指向它本身。这个操作也对后面的接口函数有比较好的影响。

②结点创建DLListCreateMemory

DLListNode* DLListCreateMemory ( DLListDataType x );

与单链表的大差不差:

//创建结点
DLListNode* DLListCreateMemory(DLListDataType x)
{
	DLListNode* newnode = (DLListNode*)malloc(sizeof(DLListNode));
	//省略if判断
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

③数据打印DLListPrint

void DLListPrint ( DLListNode* phead ); 

打印上就与单链表显得不太一样了,单链表尾结点的next指向NULL,因此循环条件定为遍历不为NULL继续,为NULL停止,可以依次打印出单链表各个结点存储的数据,但是双向循环链表,尾结点存储的next又指向了哨兵位phead,循环条件是有区别的,从何处开始打印也需要注意。

对于双向循环带头链表而言,我们从phead->next开始是第一个有效结点,一直到phead结束为整个链表的结点,那么我们创建指针cur进行遍历,cur从phead->next开始依次打印数据,直到cur指向哨兵位即cur == phead停止。

//打印数据
void DLListPrint(DLListNode* phead)
{
	assert(phead);
	printf("哨兵位");
	//循环带头链表,尾结点的next指向哨兵位,哨兵位的prev指向尾结点
	DLListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("==>%d", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

④数据销毁DLListDestroy

DLListNode* DLListDestroy ( DLListNode* phead ); 

创建一个指针cur依次释放空间,注意要保存后一个结点,循环条件为cun->next != phead;那么销毁到最后就会只剩一个哨兵位空间,最后释放掉哨兵位空间,然后返回NULL给我们在主函数创建的结构体指针变量即可,这也是为了防止野指针。

//销毁
DLListNode* DLListDestroy(DLListNode* phead)
{
	assert(phead);
	DLListNode* cur = phead->next;
	while (cur != phead)
	{
		DLListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	return NULL;
}

如果不返回NULL,我们也可以传入二级指针来操作,或者我们在进行完销毁操作后再置空也可以

⑤数据查找DLListSearch

DLListNode* DLListSearch ( DLListNode* phead, DLListDataType x ); 

查找并返回该结点,没有则返回NULL:

//查找---找到返回结点地址,未找到返回NULL
DLListNode* DLListSearch(DLListNode* phead, DLListDataType x)
{
	assert(phead);
	DLListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
}

下面是头尾插删操作。

⑥尾插DLListPushBack

void DLListPushBack ( DLListNode* phead, DLListDataType x );

我们不再需要向单链表那样遍历得到尾结点再来插入结点,可以直接通过哨兵位的prev指针得到尾结点,然后插入新的结点。那么对于双向循环带头链表而言,需要改变哨兵位的prev指向,原尾结点的next指向,然后对于新插入尾结点的next与pre指针指向进行赋值---next指向哨兵位、prev指向原尾结点。

//尾插
void DLListPushBack(DLListNode* phead, DLListDataType x)
{
	assert(phead);
	DLListNode* tail = phead->prev;//找尾
	DLListNode* newnode = DLListCreateMemory(x);
	//尾插结点的next指向哨兵位,prev指向原尾结点
	newnode->next = phead;
	newnode->prev = tail;
	//哨兵位的prev指向尾插结点,原尾结点的next指向尾插结点
	phead->prev = newnode;
	tail->next = newnode;
}

⑦头插DLListPushFront

void DLListPushFront ( DLListNode* phead, DLListDataType x ); 

对于带头循环双向链表而言,最容易出错的部分就是指针指向的修改,往往会有多个指针需要修改指向,我们要将其理清楚。对于头插,我们需要将原一号结点的prev指向插入结点,哨兵位的next指向插入结点,同时将插入结点的next指向原一号结点,prev指向哨兵位。

//头插
void DLListPushFront(DLListNode* phead, DLListDataType x)
{
	assert(phead);
	DLListNode* cur = phead->next;
	DLListNode* newnode = DLListCreateMemory(x);
	phead->next = newnode;
	newnode->next = cur;
	newnode->prev = phead;
	cur->prev = newnode;
}

⑧尾删DLListPopBack

void DLListPopBack ( DLListNode* phead ); 

首先要判断链表是否为空,为空不能删除,使用一个assert宏断言phead->next即可,如果phead->next与phead相同,那么说明该链表是空链表,仅有一个哨兵位:

assert(phead->next != phead);//没有数据--->不允许删除

尾删我们首先通过哨兵位phead的prev指针找到尾结点tail,由于删除尾结点需要对尾结点上一个结点的next指向进行修改,那么我们以同样方式找到尾结点tail的上一个结点tailprev,将tailprev的next指向哨兵位,将哨兵位的prev指针指向tailprev,然后释放tail结点即可。

//尾删
void DLListPopBack(DLListNode* phead)
{
	assert(phead);//哨兵位不能开辟失败
	assert(phead->next != phead);//没有数据--->不允许删除
	//找尾与尾的前一位
	DLListNode* tail = phead->prev;
	DLListNode* tailprev = tail->prev;
	//尾删操作
	free(tail);
	tailprev->next = phead;
	phead->prev = tailprev;
}

⑨头删DLListPopFront

void DLListPopFront ( DLListNode* phead ); 

头删需要找到第一个有效结点cur与第二个有效结点curnext,将其存储的prev指针指向哨兵位,同时将哨兵位next结点指向第二个结点curnext,释放cur结点空间即可。

//头删
void DLListPopFront(DLListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	DLListNode* cur = phead->next;
	DLListNode* curnext = cur->next;
	phead->next = curnext;
	curnext->prev = phead;
	free(cur);
}

同尾删,如果空链表,不允许删除,如果删除操作继续,我们会发现哨兵位被释放了,那么这个操作会对后续的结构体指针变量的使用产生巨大影响,该指针变量就是野指针! 

⑩pos结点前插入DLListInsert

void DLListInsert ( DLListNode* pos, DLListDataType x ); 

在pos结点前插入结点,通常搭配着查找接口函数使用,那么我们需要找到pos原来前一个结点,假设为posprev,那么将posprev的next指向插入结点,pos的prev指向插入结点,再将插入结点的next与prev分别指向pos与posprev即可。

//pos前插入x
void DLListInsert(DLListNode* pos, DLListDataType x)
{
	assert(pos);
	DLListNode* newnode = DLListCreateMemory(x);
	DLListNode* posprev = pos->prev;
	posprev->next = newnode;
	pos->prev = newnode;
	newnode->next = pos;
	newnode->prev = posprev;
}

DLListInsert接口可以完成尾插与头插:

尾插--->DLListInsert ( phead , x ) ;

头插--->DLListInsert ( phead->next , x ) ;

只需要改变传入的结点即可,传入哨兵位即为尾插;传入第一个有效结点即为头插。

pos可不可以指向哨兵位呢?

是可以的,pos指向哨兵位时在pos前插入,相当于尾插,因为posprev就是原尾结点,相当于在尾结点的后面插入了一个结点。所以不需要额外进行判断。

⑪删除pos结点DLListErase

void DLListErase ( DLListNode* pos );

对于删除操作而言,不能删除哨兵位因此需要断言:

	assert(pos);
	assert(pos->next != pos);//不能删除哨兵位

删除pos结点,那么需要改变前一个prev结点的next指向,改变后一个next结点的prev指向,然后释放pos结点空间。

//删除pos结点
void DLListErase(DLListNode* pos)
{
	assert(pos);
	assert(pos->next != pos);
	DLListNode* posprev = pos->prev;
	DLListNode* posnext = pos->next;
	free(pos);
	pos = NULL;
	posprev->next = posnext;
	posnext->prev = posprev;
}

其实在接口里pos置空还不行,因为传入的实参pos仍然是野指针,我们需要在DLListErase函数外对pos置空。即DLListErase操作结束后对pos实参置空。

DLListErase接口可以完成头删和尾删:

尾删--->DLListErase ( phead->prev ) ;

头删--->DLListErase ( phead->next ) ; 

十分钟写一个链表可不可能?

是可以做到的,我们写一个双向带头循环链表,头尾插删写一个DLListInsert与一个DLListErase即可完成。

四、顺序表与链表的区别

不同点顺序表链表
存储空间上逻辑、物理上均连续逻辑上连续、物理上不连续
随机访问随机访问1个数据---O(1)随机访问1个数据---需要遍历因此O(N)
任意位置插入或删除元素挪位---O(N),效率低只需要改变指针指向
插入结点空间扩容---1.浪费空间 2.开辟空间存在消耗插入一个开辟一块,无容量概念
应用场景元素高效存储、频繁访问任意位置的频繁插入删除---均O(1)
缓存利用率

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

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

相关文章

Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:智行无忧停车场管理系统(前后端源码 + 数据库 sql 脚本)

🔥博客主页: 【小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录 1.0 项目介绍 1.1 项目功能 2.0 用户登录功能 3.0 首页界面 4.0 车辆信息管理功能 5.0 停车位管理功能 6.0 入场登记管理功能 7.0 预约管理功能 8.0 收费规则功能 9.0…

【text2sql】低资源场景下Text2SQL方法

SFT使模型能够遵循输入指令并根据预定义模板进行思考和响应。如上图,、 和 是用于通知模型在推理过程中响应角色的角色标签。 后面的内容表示模型需要遵循的指令,而 后面的内容传达了当前用户对模型的需求。 后面的内容代表模型的预期输出,也…

MongoDB安装|注意事项

《疯狂Spring Boot讲义》是2021年电子工业出版社出版的图书,作者是李刚 《疯狂Spring Boot终极讲义》不是一本介绍类似于PathVariable、MatrixVariable、RequestBody、ResponseBody这些基础注解的图书,它是真正讲解Spring Boot的图书。Spring Boot的核心…

基于 LLamafactory 的异步API高效调用实现与速度对比

文章目录 背景摘要简介代码实现运行结果速度对比异步调用速度同步调用速度 背景 原先经常调用各家的闭源大模型的API,如果使用同步的方式调用,速度会很慢。为了加快 API 的调用速度,决定使用异步调用 API 的方式。 摘要 通过异步方式调用大…

Linux的用户和权限【Linux操作系统】

文章目录 Linux的用户切换用户普通用户暂时以root用户的权限执行指令如何把一个普通用户加入白名单? 新建用户 Linux权限权限的组成更改权限文件/目录权限的表示方法: umask粘滞位添加粘滞位的方法 Linux的用户 Linux下有两种⽤⼾:超级用户&#xff08…

如何使用apache部署若依前后端分离项目

本章教程介绍,如何在apache上部署若依前后端分离项目 一、教程说明 本章教程,不介绍如何启动后端以及安装数据库等步骤,着重介绍apache的反向代理如何配置。 参考此教程,默认你已经完成了若依后端服务的启动步骤。 前端打包命令使用以下命令进行打包之后会生成一个dist目录…

优先算法 —— 滑动窗口系列 - 无重复字符的最长子串

目录 前言 1. 无重复字符的最长子串 2. 题目解析 3. 算法原理 解法1:暴力枚举 哈希表(判断字符是否有重复出现) 解法2:滑动窗口 4. 代码 前言 当我们发现暴力解法两个指针都不回退,都是向同一个方向移动的时候我…

2024年认证杯SPSSPRO杯数学建模B题(第一阶段)神经外科手术的定位与导航解题全过程文档及程序

2024年认证杯SPSSPRO杯数学建模 B题 神经外科手术的定位与导航 原题再现: 人的大脑结构非常复杂,内部交织密布着神经和血管,所以在大脑内做手术具有非常高的精细和复杂程度。例如神经外科的肿瘤切除手术或血肿清除手术,通常需要…

Jest timers

引入 我们自己先写一个定时器,在这里,这个测试是一定会通过的,因为他一旦传入callback,就算是完成了,而不是在意你的运行结果了,而且你的定时器还有几秒呢 export const timer (fn) > {setTimeout(() > {fn()}, 3000) }//test import {timer} from "./timer"…

数据链路层(四)---PPP协议的工作状态

1 PPP链路的初始化 通过前面几章的学习,我们学了了PPP协议帧的格式以及组成,那么对于使用PPP协议的链路是怎么初始化的呢? 当用户拨号上网接入到ISP后,就建立起了一条个人用户到ISP的物理链路。这时,用户向ISP发送一…

UE5 C++ 不规则按钮识别,复选框不规则识别 UPIrregularWidgets

插件名称:UPIrregularWidgets 插件包含以下功能 你可以点击任何图片,而不仅限于矩形图片。 UPButton、UPCheckbox 基于原始的 Button、Checkbox 扩展。 复选框增加了不规则图像识别功能,复选框增加了悬停事件。 欢迎来到我的博客 记录学习过…

Latex转word(docx)或者说PDF转word 一个相对靠谱的方式

0. 前言 投文章过程中总会有各种各样的要求,其中提供word格式的手稿往往是令我头疼的一件事。尤其在多公式的文章中,其中公式转换是一个头疼的地方,还有很多图表,格式等等,想想就让人头疼欲裂。实践中摸索出一条相对靠…

Leetcode打卡:棋盘上有效移动组合的数目

执行结果:通过 题目:2056 棋盘上有效移动组合的数目 有一个 8 x 8 的棋盘,它包含 n 个棋子(棋子包括车,后和象三种)。给你一个长度为 n 的字符串数组 pieces ,其中 pieces[i] 表示第 i 个棋子的…

Day5:生信新手笔记 — R语言基本语法

一、数据类型 &#xff08;重点只有两个&#xff0c;剩下的不看&#xff09; 1.1 向量&#xff08;vector&#xff09; 矩阵&#xff08;Matrix&#xff09; 数组&#xff08;Array&#xff09; 1.2 数据框&#xff08;Data frame&#xff09; x<- c(1,2,3) #常用的向…

【Win11的Bug】无法在文件夹中创建txt文件

问题 右键只能新建文件夹 , 无法新建txt文本文档 解决办法 将注册表中的一个参数从1改为0即可. 具体内容: WinR输入regeditHKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System 将1改为0(下面这张图我已改过) 4.然后重新启动电脑即可 小技…

word如何快速创建目录?

文章目录 1&#xff0c;先自己写出目录的各级标题。2、选中目标标题&#xff0c;然后给它们编号3、给标题按照个人需求开始分级4、插入域构建目录。4.1、利用快捷键插入域构建目录4.2、手动插入域构建目录 听懂掌声&#xff01;学会了吗&#xff1f; 前提声明&#xff1a;我在此…

Java程序调kubernetes(k8s1.30.7)core API简单示例,并解决403权限验证问题,即何进行进行权限授权以及验证

简单记录问题 一、问题描述 希望通过Java程序使用Kubernetes提供的工具包实现对Kubernetes集群core API的调用&#xff0c;但是在高版本上遇见权限验证问题4xx。 <dependency><groupId>io.kubernetes</groupId><artifactId>client-java</artifact…

合合信息扫描全能王线下体验活动:科技与人文的完美交融

文章目录 前言签到欢迎仪式产品体验智能高清滤镜去除透字效果照片高清修复 破冰行动会议感受 前言 作为合合信息旗下扫描全能王的忠实粉丝&#xff0c;上周&#xff0c;我很荣幸参与了扫描全能王“扫出你的能量buff”快闪活动及技术交流会。这次活动的不仅让我对这款强大的文档…

【工具变量】上市公司企业所在地城市等级直辖市、副省级城市、省会城市 计划单列市(2005-2022年)

一、包含指标&#xff1a; 股票代码 股票代码 股票简称 年份 所属城市 直辖市&#xff1a;企业所在地是否属于直辖市。1是&#xff0c;0否。 副省级城市&#xff1a;企业所在地是否属于副省级城市。1是&#xff0c;0否。 省会城市&a…

Svn如何切换删除账号

记录Svn清除切换账号 1.首先打开小乌龟的设置如下图 打开设置后单击已保存数据&#xff0c;然后选择清除 接上图选择清除后&#xff0c;就可以打勾选择清除已保存的账号&#xff0c;我们再次检出的就可以切换账号了 &#x1f449;总结 本次记录Svn清除切换账号 如能帮助到你…