【数据结构】结构最复杂实现最简单的双向带头循环链表

news2024/9/22 9:58:05

【数据结构】结构最复杂实现最简单的双向带头循环链表

  • 一、前言
  • 二、目标
  • 三、实现
    • 1、初始化工作
    • 2、尾插
      • 2.1、图解思路
      • 2.2、代码实现
    • 3、尾删
      • 3.1、图解思路
      • 3.2、代码实现
    • 4、打印链表
    • 5、头插
      • 5.1、图解思路
      • 5.2、代码实现
    • 6、头删
      • 6.1、图解思路
      • 6.2、代码实现
    • 7、查找
    • 8、随机插入
      • 8.1、图解思路
      • 8.2、代码实现
    • 9、随机删除
      • 9.1、图解思路
      • 9.2、代码实现
    • 10、链表的销毁

一、前言

有人可能会疑惑为什么突然就从结构最简单的单向无头非循环链表转到结构最复杂度双向带头循环链表,这好像跨度有点大啊。
但这两个链表是两个很好的极端,单向无头非循环链表虽然结构最简单:
在这里插入图片描述
但也正因为其结构最简单给它带来了很多局限性,比如对于每个节点我们只能找到它的后继而不能找到它的前驱,尾插的时候总是需要从头遍历找到尾节,总是要特殊处理头指针的改变等等……
所以单向无头非循环链表实现起来应该是最难的。
而双向带头循环链表虽然结构最复杂:
在这里插入图片描述
但也正是因为它复杂度结构使得它在使用的时候有更多的选择,比如对于每个节点,我们不仅能找到它的后继,也能找到它的前驱,或者是找尾很方便。
所以双向带头循环链表的实现应该是最简单的。
所以只要搞清楚了这两个链表,那我们再看其他结构的链表的时候也就游刃有余了。

二、目标

双向带头循环链表所需要实现的功能大致如下:

// 初始化链表
void InitList(ListNode* head);
// 创建一个新节点,返回节点指针
ListNode* create_newnode(data_type x);
// 双向循环链表的尾插
void double_circle_list_push_back(ListNode* head, data_type x);
// 双向循环链表的尾删
void double_circle_list_pop_back(ListNode *head);
// 双向循环链表的打印
void print_double_circle_list(ListNode* head);
// 双向循环链表的头插
void double_circle_list_push_front(ListNode* head, data_type x);
// 双向循环链表的头删
void double_circle_list_pop_front(ListNode* head);
// 双向循环链表的查找,返回查找到的节点的指针,若找不到则返回NULL
ListNode* find_Node(ListNode* head, data_type x);
// 双向循环链表的随机插入,在目标节点的后面插入新节点
void double_circle_list_insert(ListNode* target, data_type x);
// 双向循环链表的随机删除,从链表中删除目标节点
void double_circle_list_remove(ListNode* target);
// 双向循环链表的销毁
void destroy_double_circle_list(ListNode* head);

其实这跟单链表的功能也大差不差,只是实现的思路不一样而已。
那接下来就让我们一个一个的实现吧。

三、实现

1、初始化工作

同样的我们还是先把节点类型定义了和数据域的数据类型重定义了:

// 将数据类型重定义
typedef int data_type;

// 创建节点类型
typedef struct double_circle_list_node {
	data_type val;
	struct double_circle_list_node* prev;
	struct double_circle_list_node* next;
} ListNode ;

接下来我们就可以对已有的链表头节点进行初始化了,循环链表必须保证在任何情况下都成环,所以我们初始化时,应该把头节点的next和prev都指向它自己:
在这里插入图片描述
代码如下:

// 初始化链表
void InitList(ListNode* head) {
	assert(head);
	head->prev = head;
	head->next = head;
}

而又因为我们后面在写个各个插入节点的函数的时候需要动态开辟一个新的节点,所以我们就行一个函数用来创建一个新节点,返回节点指针:

// 创建一个新节点,返回节点指针
ListNode* create_newnode(data_type x) {
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
	if (NULL == newNode) {
		perror("malloc fail\n");
		exit(-1);
	}
	newNode->val = x;
	return newNode;
}

这样我们的初始化工作就都完成了。

2、尾插

2.1、图解思路

比起单链表,双向循环链表找起尾来简直不要太简单,因为头节点的prev始终都指向链表的最后一个节点,所以尾节点tail即为head->prev:
在这里插入图片描述
所以我们创建好新的节点后就可以直接将各个指针指向相应的地方了:
在这里插入图片描述
分别是:

newNode->prev = tail;
tail->next = newNode;
newNode->next = head;
head->prev = newNode;

当然这四个指针的更改顺序随便打乱也是毫无影响的。
而我们是否需要额外考虑链表中只有一个头节点的情况呢?
其实是不用的,因为如果链表中只有一个头结点,那么按照上面的逻辑,tail指向的也还是头节点:
在这里插入图片描述
这时候我们再执行上面一样的逻辑,也还是能达到效果的:
在这里插入图片描述

2.2、代码实现

// 双向循环链表的尾插
void double_circle_list_push_back(ListNode* head, data_type x) {
	assert(head);
	// 先创建新节点
	ListNode* newNode = create_newnode(x);
	ListNode* tail = head->prev;

	// 插入
	newNode->prev = tail;
	tail->next = newNode;

	newNode->next = head;
	head->prev = newNode;
}

3、尾删

3.1、图解思路

因为是双向链表,所以我们就可以直接找到tail的前一个节点,所以我们的尾删当然也很简单啦:
在这里插入图片描述
如图,我们可以想让两个指针tail和tailPre分别指向尾节点和尾节点的前一个节点,然后我们可以先释放表tail再修改相应的指针或先修改相应的指针再释放都行,这两个操作的先后顺序不影响结果:
在这里插入图片描述
当然我们这里也并不需要考虑链表中只剩一个有小姐点的情况,对于双向循环链表来说这些都是一步到位的。
当然了,我们传入的head是不能为空的,并且也要保证链表中至少有一个有效节点,不能只有一个头节点head,因为不能把head也给删。

3.2、代码实现

// 双向循环链表的尾删
void double_circle_list_pop_back(ListNode *head) {
	assert(head);
	assert(head->next != head);
	ListNode* tail = head->prev;
	ListNode* tailpre = tail->prev;
	free(tail);
	tailpre->next = head;
	head->prev = tailpre;
}

4、打印链表

打印其实是和单向链表是一样的,我们从前往后遍历链表中的节点然后又打印出节点信息即可,只不过头节点是不用打印的,所以我们可以直接从head的next开始打印。
而因为循环链表是没有空节点的,所以我们循环结束的条件就不是为空了,但我们可以判断时候回到了头节点head,因为头节点是不用打印的:

// 双向循环链表的打印
void print_double_circle_list(ListNode* head) {
	assert(head);
	if (head->next == head) {
		printf("NULL\n");
		return;
	}
	ListNode* cur = head->next;
	printf("[head]∞");
	while (cur != head) {
		printf("[%d]∞", cur->val);
		cur = cur->next;
	}
	printf("\n");
}

打印效果如下:
在这里插入图片描述

5、头插

5.1、图解思路

头插的操作几乎和单链表的相同,只不过多出了个prev指针需要我们处理而已:
在这里插入图片描述

如上如,我们可以先用一个first指针指向第一个节点,然后各个指针的修改顺序就可以随意打乱也不会影响结果了。
当然,只有一个头节点的情况也是不需要特殊考虑的,直接用相同的操作即可。

5.2、代码实现

// 双向循环链表的头插
void double_circle_list_push_front(ListNode* head, data_type x) {
	assert(head);
	ListNode* newNode = create_newnode(x);
	ListNode* first = head->next;
	newNode->next = first;
	first->prev = newNode;

	head->next = newNode;
	newNode->prev = head;
}

6、头删

6.1、图解思路

头删其实也是和单链表只差了一个prev指针:
在这里插入图片描述
我们可以先将新的第一个节点(第二个节点)用一个newfirst指针先存起来,然后在修改相应的指针指向。

6.2、代码实现

// 双向循环链表的头删
void double_circle_list_pop_front(ListNode* head) {
	assert(head );
	assert(head->next != head);

	ListNode* newfirst = head->next->next; // 新的第一个节点
	free(head->next);

	head->next = newfirst;
	newfirst->prev = head;
}

7、查找

查找那就和单向链表一模一样了,就像打印链表一样从前往后遍历节点,找到只相同的节点直接返回即可:

// 双向循环链表的查找,根据数据域的值查找,返回查找到的节点的指针,若找不到则返回NULL
ListNode* find_Node(ListNode* head, data_type x) {
	assert(head && head->next != head);
	ListNode* cur = head->next;
	while (cur != head) {
		if (cur->val == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

8、随机插入

8.1、图解思路

通过传入目标节点的指针,实现在目标节点的后面插入一个新节点。
这个函数需要配合着上面的查找函数一起使用,先用查找函数找到目标节点,再将目标节点传入插入函数就可以实现随机插入了。
置于插入的思路就不用多说了,基本和尾插头插是一个逻辑。

8.2、代码实现

// 双向循环链表的随机插入,在目标节点的后面插入新节点
void double_circle_list_insert(ListNode* target, data_type x) {
	assert(target);
	ListNode* newNode = create_newnode(x);
	ListNode* next = target->next;
	target->next = newNode;
	newNode->prev = target;

	newNode->next = next;
	next->prev = newNode;
}

9、随机删除

9.1、图解思路

通过传入的目标节点,将目标节点从链表中删除。
获取单链表还需要从前往后遍历找到目标节点的前一个节点,但双向链表就不用。
我们直接执行target->prev->next = target->next和target->next->prev = target->prev,然后将在释放掉target即可:
在这里插入图片描述
当然了,直接这样操作的话可能会有的朋友会晕,如果觉得晕的话可以多定义两个指针,这样就容易理解一点。

9.2、代码实现

// 双向循环链表的随机删除,从链表中删除目标节点
void double_circle_list_remove(ListNode* target) {
	assert(target);
	target->prev->next = target->next;
	target->next->prev = target->prev;
	free(target);
}

10、链表的销毁

销毁也是和单链表的一样的:

// 双向循环链表的销毁
void destroy_double_circle_list(ListNode* head) {
	assert(head && head->next != head);
	ListNode* cur = head->next;
	ListNode* next = cur->next;
	while (cur != head) {
		next = cur->next;
		free(cur);
		cur = next;
	}
}

这里并没有传入二级指针来达到在函数内销毁头节点,但我们在使用时需要记得最后要连头节点head也销毁了。

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

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

相关文章

数据结构与算法基础(青岛大学-王卓)(2)

第二弹火爆来袭中 这波是单链表的内容整理,废话不多说,上小龙虾呀(又到了龙虾季节了,哎,口水直流了~~) beautiful的分割线 文章目录 第二弹火爆来袭中这波是单链表的内容整理,废话不多说,上小龙虾呀(又到了…

【致敬未来的攻城狮计划】— 连续打卡第二十七天:瑞萨RA RA2E1 的 BTN触摸按键

文章目录 由于一些特殊原因: 系列文章链接:(其他系列文章,请点击链接,可以跳转到其他系列文章)或者参考我的专栏“ 瑞萨MCU ”,里面是 瑞萨RA2E1 系列文章。 24.RA2E1的 DMAC——数据传输 25.R…

DB2_sql_问题

db2新增字段指定顺序 这个是不能做到的,除非把表删除重新创建的! 原理是这样子的:当你创建表时系统会记录下你的SEQ-ID,就是字段的顺序号,这个是根据字段先后顺序来生成的,系统默认显示的时候也是根据这个来的&#x…

linux:工具(命令)vi、vim文本编辑器详解。

linux:工具(命令)vi/vim文本编辑器详解。 因此,本质上vi和vim是同种东西,后面也会合起来说,但是使用上会使用vim,因为vim是加强版。 使用形式: 无论退出还是进入都需要去到 “命令模式”。 当使用vi/vim时就会进入“命…

「高性能MySQL」读书笔记(1)- MySQL架构

一、前言 本系列主要是记录阅读「高性能MySQL」期间笔记,记录在日常使用中忽略的知识、模糊的点,主要面对有一定MySQL使用经验的开发者。 本文是针对于MySQL一些基础定义的解释说明,会非常浅显通俗易懂。 二、MySQL的逻辑架构 简单梳理My…

PCL学习九:Registration-配准

参考引用 Point Cloud Library黑马机器人 | PCL-3D点云 1. 点云中的数学 函数求导 对于函数 f ( x ) x 2 f(x)x^2 f(x)x2 其一阶导数也是 x x x 的函数: d f d x 2 x \frac{df}{dx}2x dxdf​2x其二阶导为常数,与 x x x 无关: d 2 f d x…

【漏洞分析】CVE-2021-0920 Linux内核垃圾回收机制中的竞争UAF漏洞

漏洞发现:该漏洞早在2016年被 RedHat 内核开发人员发现并披露,但 Linux 内核社区直到 2021 年重新报告后才对该漏洞进行修补(patch)。Google的威胁分析小组(Threat Analysis Group)发现该漏洞在野外被使用&…

shell脚本----基础命令

文章目录 一、sort命令二、uniq命令三、 tr命令四、cut命令 一、sort命令 sort命令以行为单位对文件内容进行排序,也可以根据不同的数据类型来排序,比较的原则是从首字符向后,一次按ASCII码的值进行比较,最后按序输出。 ASCII码…

【P17】JMeter 边界提取器(Boundary Extractor)

文章目录 一、准备工作二、测试计划设计 一、准备工作 慕慕生鲜: http://111.231.103.117/#/login 进入网页后,登录,页面提供了账户和密码 搜索框输入“虾” 右键检查或按F12,打开调试工具,点击搜索 二、测试计划设…

详细版易学版TypeScript - 元组 枚举

一、元组(Tuple) 数组:合并了相同类型的对象 const myArr: Array<number> [1, 2, 3]; 元组(Tuple):合并了不同类型的对象 // 定义元组时就要确定好数据的类型&#xff0c;并一一对应 const tuple: [number, string] [12, "hi"]; // 添加内容时&#xff0c;不…

SQLIST数据库编程

目录 数据库简介 1.常用数据库 2. SQLite基础 3.创建SQLite数据库 虚拟中sqlite3安装 基础SQL语句使用 sqlite3编程 数据库简介 1.常用数据库 大型数据库 &#xff1a;Oracle 中型数据库 &#xff1a;Server是微软开发的数据库产品&#xff0c;主要支持windows平台 小型数据库…

( 位运算 ) 190. 颠倒二进制位 ——【Leetcode每日一题】

❓190. 颠倒二进制位 难度&#xff1a;简单 颠倒给定的 32 位无符号整数的二进制位。 提示&#xff1a; 请注意&#xff0c;在某些语言&#xff08;如 Java&#xff09;中&#xff0c;没有无符号整数类型。在这种情况下&#xff0c;输入和输出都将被指定为有符号整数类型&a…

Vue.js自定义指令及用Vue实现简单的学生信息管理系统

目录 一、自定义指令v-mycolor 自定义指令生命周期&#xff1a; 二、使用钩子函数的自定义指令 三、Vue实现简单的学生信息管理系统 除了核心功能默认内置的指令&#xff0c;Vue.js允许注册自定义指令。添加一个自定义指令&#xff0c;有两种方式&#xff1a; &#xff08;1…

Redis 常见命令

一、redis中的常见数据结构 Redis共有5种常见数据结构&#xff0c;分别字符串&#xff08;STRING)、列表&#xff08;LIST&#xff09;、集合&#xff08;SET)、散列&#xff08;HASH&#xff09;、有序集合&#xff08;ZSET)。 二、redis中字符串(String)介绍 String 类型是…

PS网页版设计工具有哪些?

Photoshop是平面设计领域的老熟人&#xff0c;也是许多设计师的启蒙设计软件。然而&#xff0c;Photoshop的功能繁多&#xff0c;需要设计师具备较强的软件操作能力。在我们以为会和Photoshop一直相爱相杀的时候&#xff0c;一款专注于用户界面的矢量设计软件——即时设计&…

荔枝派Zero(全志V3S)驱动开发之RGB LCD屏幕显示jpg图片

文章目录 前言一、jpeglib 库移植1、jpeglib 库下载2、安装 jpeglib 库 二、jpeg 图片解压缩过程和压缩过程1、jpeg 解压缩过程2、jpeg 压缩过程 三、编译 C 源码1、源码展示2、拷贝需要用到的头文件3、编译 C 代码 四、验证测试1、拷贝相关文件到开发板2、显示图片 前言 由于…

深入了解Dubbo SPI 工作机制——@Adaptive(6)

Adaptive这个注解就是适配策略&#xff0c;我都是称呼为最佳适配子类&#xff0c;或者最佳适配类。就是找到最佳的子实现类的&#xff0c;其实就是默认的类。这个注解可以打在类上方&#xff0c;那么dubbo SPI机制通过接口获取实例类&#xff0c;就是获取到有Adaptive注解的实现…

WooCommerce商城开发:高性能订单存储数据库模式

这是一系列深入探讨的第一部分&#xff0c;专门用于解释高性能订单存储数据库模式的实施。 与1 月份提出的版本相比&#xff0c;数据库模式的变化很小。我们在不同的地方添加和删除了几列&#xff0c;但整体表结构与第一个提案中描述的相同&#xff1a; 我们在此项目中添加了4…

51单片机(九)LED点阵屏

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

创维E900-S-Hi3798MV100-当贝纯净桌面-卡刷固件包

创维E900-S-Hi3798MV100-当贝纯净桌面-卡刷固件包-内有教程 特点&#xff1a; 1、适用于对应型号的电视盒子刷机&#xff1b; 2、开放原厂固件屏蔽的市场安装和u盘安装apk&#xff1b; 3、修改dns&#xff0c;三网通用&#xff1b; 4、大量精简内置的没用的软件&#xff0…