编织数据结构的艺术:双向带头循环链表的华丽实现

news2024/11/28 4:36:17

在这里插入图片描述

上一篇博客,我们了解并实现了单向+不带头+不循环链表,而本篇博客会讲解链表中的王者:双向+带头+循环链表。

概述

双向+带头+循环链表的特点是:

  1. 每个结点内部,既有指向上一个结点的前驱指针prev,也有指向下一个结点的后继指针next。
  2. 第一个结点,是哨兵位的头结点,不存储有效数据。从第二个结点开始存储有效数据。
  3. 最后一个结点的后继指针指向第一个结点,第一个结点的前驱指针指向最后一个结点。

这个结构,感觉上就是单向+不带头+不循环链表完全反过来!那么这么设计有什么优点呢?看过我的上一篇博客的朋友应该都能感觉到,单链表的实现相当麻烦,看起来结构简单,但是实现起来复杂、逻辑复杂、效率低。事实上,本篇博客讲解的双链表看起来结构复杂,但是实现起来逻辑简单、效率高。具体等实现之后大家体会更深。

双链表结点的声明如下:

// 链表存储的数据类型
typedef int LTDataType;
// 带头+双向+循环链表
typedef struct ListNode
{
	LTDataType data;       // 存储数据
	struct ListNode* next; // 指向下一个结点
	struct ListNode* prev; // 指向上一个结点
}ListNode;

申请结点

先写一个函数,向堆区申请一个结点,函数的声明如下:

ListNode* BuyListNode(LTDataType x);

使用malloc函数申请一个结点,并对data、next、prev进行初始化即可。

ListNode* BuyListNode(LTDataType x)
{
	// 创建新结点
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
	// 判断是否创建成功
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	// 初始化
	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;

	return newnode;
}

初始化

接下来写一个函数来创建一个链表,并返回哨兵位的头结点。函数的声明如下:

ListNode* ListCreate();

根据双向+带头+循环链表的定义,我们需要开辟一个哨兵位的头结点,这个结点不存储有效的数据。根据“循环”的特点,当只有一个结点时,它的prev和next都是它自己。

ListNode* ListCreate()
{
	// 创建哨兵位
	ListNode* newnode = BuyListNode(0);
	// 链接
	newnode->next = newnode;
	newnode->prev = newnode;

	return newnode;
}

销毁

接下来写一个函数来销毁链表,函数声明如下:

void ListDestroy(ListNode* phead);

遍历双向链表,并依次销毁即可。遍历时需要注意:从phead->next开始,等于phead时结束。销毁之前要保存下一个结点,否则就找不到下一个了。把其他结点都销毁完后,再销毁哨兵位。

注意需要检查phead指针的有效性,因为哪怕链表为空,也有哨兵位,phead一定不为空,后面的函数同理。

void ListDestroy(ListNode* phead)
{
	assert(phead);

	ListNode* del = phead->next;
	// 遍历+删除
	while (del != phead)
	{
		ListNode* next = del->next;
		// 删除
		free(del);
		// 迭代
		del = next;
	}

	// 释放哨兵位
	free(phead);
	phead = NULL;
}

打印

下面写一个函数来打印链表中的数据,方便后面的测试。这是函数声明:

void ListPrint(ListNode* phead);

和上一个函数类似,也是从phead->next开始遍历,到phead结束。

void ListPrint(ListNode* phead)
{
	assert(phead);

	ListNode* cur = phead->next;
	printf("哨兵位");
	// 遍历+打印
	while (cur != phead)
	{
		printf("<==>%d", cur->data);
		// 迭代
		cur = cur->next;
	}
	printf("\n");
}

判空

接下来写一个函数,来判断链表是否为空链表。这是函数声明:

bool ListEmpty(ListNode* phead);

链表什么时候为空呢?当链表只有一个哨兵位的头结点时,链表中是没有有效数据的,此时我们认为链表为空。具体的判断就只需要判断phead->next和phead是否相等即可。

bool ListEmpty(ListNode* phead)
{
	assert(phead);

	return phead->next == phead;
}

查找

再来个简单的函数,在链表中查找数据。函数的声明如下:

ListNode* ListFind(ListNode* phead, LTDataType x);

和前面的遍历完全一样。从phead->next开始遍历数据,遇到phead结束。

ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);

	ListNode* cur = phead->next;
	// 遍历+查找
	while (cur != phead)
	{
		if (cur->data == x)
		{
			// 找到了
			return cur;
		}
		// 迭代
		cur = cur->next;
	}

	// 没找到
	return NULL;
}

插入

下面我们来实现一个函数,在链表中的指定结点前面插入一个新的结点。函数的声明如下:

void ListInsert(ListNode* pos, LTDataType x);

由于双向链表的特性,我们知道了pos,就可以找到pos的前一个结点prev,然后在prev和pos中间插入新结点即可。

void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* newnode = BuyListNode(x);

	// 链接prev<==>newnode<==>pos
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

有了Insert函数,我们就可以实现尾插和头插了。尾插就是在哨兵位的头结点前面插入,头插就是在phead->next前面插入。

void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);

	ListInsert(phead, x);
}

void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);

	ListInsert(phead->next, x);
}

删除

最后实现一个函数,删除链表中的指定结点,函数的声明如下:

void ListErase(ListNode* pos);

找到pos的前一个结点prev和后一个结点next,删除pos结点,链接prev和next即可。

void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* next = pos->next;

	// 释放pos
	free(pos);
	pos = NULL;
	// 链接
	prev->next = next;
	next->prev = prev;
}

有了Erase函数,就可以轻松实现尾删和头删了。尾删就是删除phead->prev,头删就是删除phead->next。

严谨起见,最好先断言一下链表非空。

void ListPopBack(ListNode* phead)
{
	assert(phead);
	// 断言链表非空
	assert(!ListEmpty(phead));

	ListErase(phead->prev);
}

void ListPopFront(ListNode* phead)
{
	assert(phead);
	// 断言链表非空
	assert(!ListEmpty(phead));

	ListErase(phead->next);
}

总结

呼,这就搞定了!大家可以对比一下上一篇博客中的单链表,那叫一个天上地下!真是没有对比就没有伤害。双向链表的结构确实比单链表的结构要复杂,但是由于其结构特点,没有死角,能够实现以O(1)的时间复杂度在任意位置插入删除数据,非常强大。不过链表还是有缺点的,它在内存中不是连续存放的,这就导致了其无法实现下标的随机访问,并且缓存命中率较低,存在缓存污染。

感谢大家的阅读!

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

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

相关文章

《C++入门攻略》(小白向)

函数 函数、输入、传参 在程序中使用函数时&#xff0c;你必须先声明它然后再定义声明的目的是告诉编译器你即将要定义的函数的名字是什么&#xff0c;返回值的类型是什么以及参数是什么而定义则是告诉编译器这个函数的功能是什么。假如我们不声明&#xff0c;那么该函数就不能…

vue快速上手教程与简单安装

目录 vue简介 vue实例 通过 CDN 使用 Vue NPM 方法 介绍 下载 使用 vue简介 Vue.js 是一套构建用户界面的渐进式框架。 Vue 只关注视图层&#xff0c; 采用自底向上增量开发的设计。 Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。 vue实例…

ping包过程之arp(地址解析协议)

0,怎么引出arp地址解析协议的呢&#xff1f; 在硬件层次上进行的数据帧交换必须有正确的接口地址&#xff08;既是MAC地址&#xff09;。但是&#xff0c; T C P / I P有自己的地址&#xff1a; 32 bit的I P地址。知道主机的 I P地址并不能让内核发送一帧数据给主机。内核&…

二十、Zipkin持久化链路跟踪

目录 Zipkin持久化 使用mysql数据库持久化 1、创建zipkin数据库 2、启动zipkin使用以下脚本 3、访问接口&#xff08;配置了sleuth链路跟踪&#xff09; 使用ElasticSearch持久化 zipkin启动脚本 Zipkin持久化 Zipkin server默认会将追踪数据信息保存在内存中&#xff0…

NoSQL之Redis配置与数据库常用命令

目录 一、关系型数据库与非关系型数据库概述1.1 关系型数据库1.2 非关系型数据库 二、关系型数据库与非关系型数据库的区别2.1 数据的存储方式不一样2.2 扩展的方式不同2.3 对事务性的支持不同 三、非关系型数据库产生背景四、Redis简介4.1 Redis的单线程模式4.2 Redis优点4.3 …

B2B 客户支持,如何做好?

多年来&#xff0c;基于尖端技术的现代和个性化客户体验一直主导着企业对消费者&#xff08;B2C&#xff09;领域。然而&#xff0c;在企业对企业&#xff08;B2B&#xff09;行业中&#xff0c;出色的客户体验变得比以往任何时候都更加重要。许多组织正在开发类似于B2C市场中个…

无延迟直播/超低延迟直播快速接入的示例

简要说明 接入无延迟直播/超低延迟直播播放前&#xff0c;需确保直播间频道是无延迟频道&#xff0c;SDK中使用无延迟与常规播放无异&#xff0c;只需加入若干配置就可以快速接入。 什么是无延迟/超低延迟直播&#xff0c;可参见我的这篇文章&#xff1a; 无延时直播/超低延时…

Java 基础进阶篇(十四)—— File类常用方法

File 类的对象代表操作系统的文件&#xff08;文件、文件夹&#xff09;&#xff0c;File 类在 java.io.File 包下。 File 类提供了诸如&#xff1a;创建文件对象代表文件&#xff0c;获取文件信息&#xff08;大小、修改时间&#xff09;、删除文件、创建文件&#xff08;文件…

【软件测试与质量保证】期末复习2(HITWH)(软件测试部分)

更多复习资料在下方链接获取&#xff1b;包括复习笔记&#xff08;有具体习题&#xff09;、习题答案以及设计题示例 链接&#xff1a;复习资料 复习笔记里的习题不包含答案&#xff0c;具体答案在【云班课答案】文件夹中&#xff0c;顺序都是一一对应的&#xff0c;喜欢刷题可…

进程间通信之管道篇

&#x1f3c6;一、进程间通信目的 1.1什么是通信 进程是具有独立性的&#xff0c;而我们要实现进程间通信的目标&#xff0c;是需要开辟空间和创造方法的。 通信目的&#xff1a; 1、数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程 2、资源共享&#xff1a;…

CSAPP 第六章存储器的结构层次

源程序 执行程序 空间代码都要存在外存上&#xff0c;程序运行的时候操作系统要把外存的东西加载到内存里&#xff0c;CPU要从内存一行一行的读、译码和分析 我们来看一个例子&#xff1a; 指令位于内存中的代码段中&#xff0c;必须从内存中读出来进行译码分析之后才能运行 指…

细说java动态代理及使用场景

一、定义 Java代理模式是一种结构型设计模式&#xff0c;它允许通过创建一个代理对象来间接访问另一个对象&#xff0c;从而控制对原始对象的访问。 1.1 作用 1、在访问原始对象时增加额外功能&#xff0c;如访问前或访问后添加一些额外的行为。 2、控制对原始对象的访问。 J…

热水智能控制系统有什么优点?

热水智能控制系统是一种先进的技术&#xff0c;可以极大地提高家庭和商业场所的热水使用效率&#xff0c;降低能源消耗和运营成本。这种系统利用现代化的传感器、控制器和通讯技术&#xff0c;可以智能地监测和控制热水的温度、流量和使用情况&#xff0c;并根据实际需求来调节…

ASEMI代理ADI亚德诺ADM706SARZ-REEL原厂芯片

编辑-Z ADM706SARZ-REEL参数描述&#xff1a; 型号&#xff1a;ADM706SARZ-REEL VCC工作电压范围&#xff1a;1.0-5.5V 电源电流&#xff1a;100μA 重置阈值滞后&#xff1a;20 mV 复位脉冲宽度&#xff1a;200 ms PFI输入阈值&#xff1a;1.25V PFI输入电流&#xff…

Linux 部署 scrapydweb

一、 创建虚拟环境&#xff0c;在虚拟环境下操作 1、安装scrapyd pip install scrapyd2、安装scrapyd-client pip install scrapyd-client3、安装scrapydweb pip install scrapydweb4、安装Logparser pip install Logparser二、新建一个scracyd的配置文件 sudo mkdir /etc/scr…

MySql.Data.dll 因版本问题造成报错的处理

NetCore 链接MySQL 报 Character set ‘utf8mb3‘ is not supported by .Net Framework 异常解决_character set utf8mb3_csdn_aspnet的博客-CSDN博客 查看mysql版本号&#xff0c;两种办法&#xff1a; 第一种在数据库中执行查询&#xff1a;SELECT version; 第二种使用工具…

数据治理和合规性:如何确保大数据应用遵守法规和标准

第一章&#xff1a;引言 在数字时代&#xff0c;大数据的应用日益普遍&#xff0c;对企业和组织的决策、运营和创新产生了深远的影响。然而&#xff0c;随着数据规模的不断增长&#xff0c;以及数据泄露和滥用事件的频繁发生&#xff0c;数据治理和合规性问题愈发突显。企业和…

推荐系统用户长序列建模

目录 一、背景 二、技术方案 2.1 DIN 简介 论文细节 优缺点 2.2 DINE 简介 论文细节 2.3 MIMN 简介 论文细节 2.4 SIM 简介 论文细节 优缺点 2.5 DSIN 简介 论文细节 一、背景 阿里巴巴的精排模型从传统lr&#xff0c;到深度学习&#xff0c;再到对用户长历…

使用云服务器可以做什么?十大使用场景举例说明

使用阿里云服务器可以做什么&#xff1f;阿里云百科分享使用阿里云服务器常用的十大使用场景&#xff0c;说是十大场景实际上用途有很多&#xff0c;阿里云百科分享常见的云服务器使用场景&#xff0c;如本地搭建ChatGPT、个人网站或博客、运维测试、学习Linux、跑Python、小程…

6年测试,不断磨炼升级打怪自动化测试,一路晋升他终于冲出月35k+

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Python自动化测试&…