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

链表(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)
缓存利用率

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JAVA |日常开发中读写TXT文本详解

JAVA |日常开发中读写TXT文本详解 前言一、读取 TXT 文本1.1 使用BufferedReader读取1.2 使用Scanner读取 二、写入 TXT 文本2.1 使用BufferedWriter写入2.2 使用PrintWriter写入2.3 字节流写入(FileOutputStream)后转换为字符流(…

clion解决默认编译器乱码问题

解决乱码问题 如图所示当我们在clion下开发时如果输入中文,会有乱码出现。 问题的产生 clion默认的C/C编译器(MinGW)对中文的解析有问题出现乱码。 解决方案 修改文件的编码方式 进入clion的Settings(设置)–>Editor(编辑) —>Fil…

pset2 substitution.c

1.extension:To Do Tasks 推荐一个vscode里面一个很好用的插件!!!写出解决的步骤,不但理清楚思路。还可以提高效率!特别是针对一些文本比较长的pset,要求多且零碎,反复切换页面&…

程序员需要具备哪些知识?

程序员需要掌握的知识广泛而深厚,这主要取决于具体从事的领域和技术方向。不过,有些核心知识是共通的,就像建房子的地基一样,下面来讲讲这些关键领域: 1. 编程语言: 无论你是搞前端、后端、移动开发还是嵌…

解密时序数据库的未来:TDengine Open Day技术沙龙精彩回顾

在数字化时代,开源已成为推动技术创新和知识共享的核心力量,尤其在数据领域,开源技术的涌现不仅促进了行业的快速发展,也让更多的开发者和技术爱好者得以参与其中。随着物联网、工业互联网等技术的广泛应用,时序数据库…

云计算vsphere 服务器上添加主机配置

这里是esxi 主机 先把主机打开 然后 先开启dns 再开启 vcenter 把每台设备桌面再vmware workstation 上显示 同上也是一样 ,因为在esxi 主机的界面可能有些东西不好操作 我们选择主机和集群 左边显示172.16.100.200

Kotlin报错:lateinit property xxx has not been initialized

Kotlin报错:lateinit property xxx has not been initialized 发生在定义了一个名为xxx的lateinit变量。 解决,在调用前,可以先判断一层该xxx变量是否已经初始化: if (this::xxx.isInitialized) {//正常使用该变量} kotlin.Unini…

【ArcGIS微课1000例】0134:ArcGIS Earth实现二维建筑物的三维完美显示

文章目录 一、加载数据二、三维显示三、三维符号化一、加载数据 加载配套实验数据(0134.rar中的建筑物,2d或3d都可以),方法如下:点击添加按钮。 点击【Add Files】,在弹出的Open对话框中,选择建筑物,点击确定,完成添加。 默认二维显示: 二、三维显示 右键建筑物图层…

【Linux系统编程】——理解冯诺依曼体系结构

文章目录 冯诺依曼体系结构硬件当代计算机是性价比的产物冯诺依曼的存储冯诺依曼的数据流动步骤冯诺依曼结构总结 冯诺依曼体系结构硬件 下面是整个冯诺依曼体系结构 冯诺依曼结构(Von Neumann Architecture)是现代计算机的基本结构之一,由数…

H3C OSPF实验

实验拓扑 实验需求 按照图示配置 IP 地址按照图示分区域配置 OSPF ,实现全网互通为了路由结构稳定,要求路由器使用环回口作为 Router-id,ABR 的环回口宣告进骨干区域 实验解法 一、配置IP地址 [R1]int l0 [R1-LoopBack0]ip add 1.1.1.1 32 […

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

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

Android Studio历史版本下载

Android Studio 下载文件归档 | Android Developers 一定要选择英文环境, 拉到最后,同意

纯粹直播 1.7.7 |手机版和TV版,聚合六大直播平台,原画播放

纯粹直播是一款开源的应用程序,支持兴趣化主题的游戏直播、户外直播和才艺直播节目。目前可以观看斗鱼、B站、虎牙和抖音等六大直播平台的内容。该应用适配了安卓手机和电视盒子平台使用,并且软件无广告,提供原画质播放体验。 大小&#xff…

【Docker】针对开发环境、测试环境、生产环境如何编排?

目录 一、引言 二、Docker Compose 文件基础 三、针对不同环境的 Docker 编排 开发环境 测试环境 生产环境 四、配置文件全局变量的编写 五、总结 一、引言 在软件开发和部署的过程中,不同的环境有着不同的需求和配置。Docker 作为一种强大的容器化技术&…

rabbitmq 安装延时队列插件rabbitmq_delayer_message_exchange(linux centOS 7)

1.插件版本 插件地址:Community Plugins | RabbitMQ rabbitmq插件需要对应的版本,根据插件地址找到插件 rabbitmq_delayer_message_exchange 点击Releases 因为我rabbitmq客户端显示的版本是: 所以我选择插件版本是: 下载 .ez文…

同道猎聘Q3营收降利润增,AI或成估值重塑关键词

2024年,经济向好的趋势没有改变,挑战却仍然存在。企业纷纷进行结构性变革优化或业务方向调整。这一点反映到人才市场,绝大多数企业对招聘扩张持保守态度,降本增效的主题仍在延续。 作为人才市场水温变化的“温度计”,…

UPLOAD LABS | PASS 10 - 黑名单绕过(Windows . 绕过 - 变体)

关注这个靶场的其它相关笔记:UPLOAD LABS —— 靶场笔记合集-CSDN博客 0x01:过关流程 本关的目标是上传一个 WebShell 到目标服务器上,并成功访问: 通过查看源码,可以发现,本关在之前所有关卡的基础上做了…

【iOS】多线程基础

【iOS】多线程基础 文章目录 【iOS】多线程基础前言进程与线程进程进程的状态进程的一个控制结构进程的上下文切换 线程为什么要用线程什么是线程线程和进程的关系线程的上下文切换 线程和进程的优缺点 小结 前言 笔者由于对于GCD不是很了解,导致了项目中网络请求哪…

【0x3D】HCI_Remote_Host_Supported_Features_Notification事件详解

目录 一、事件概述 二、事件格式及参数说明 2.1. HCI_Remote_Host_Supported_Features_Notification事件格式 2.2. BD_ADDR 2.3. Remote_Host_Supported_Features 三、事件作用 3.1. 设备特性沟通与理解 3.2. 功能协商与性能优化 3.3. 设备管理与配置更新 四、应用场…

等差数列末项计算

等差数列末项计算 C语言代码C 代码Java代码Python代码 💐The Begin💐点点关注,收藏不迷路💐 给出一个等差数列的前两项a1,a2,求第n项是多少。 输入 一行,包含三个整数a1,a2&#x…