数据结构 ——— C语言实现带哨兵位双向循环链表

news2024/11/24 14:04:35

 

目录

前言

无哨兵位单向不循环链表的缺陷

带哨兵位双向循环链表的概念

带哨兵位双向循环链表的结构

带哨兵位双向循环链表逻辑结构示意图​编辑

实现带哨兵位双向循环链表的准备工作

实现带哨兵位双向循环链表

1. 创建新节点

2. 初始化哨兵位

3. 定义哨兵位指针

4. 打印链表所有节点的数据

5. 在链表尾部插入节点

6. 在链表头部插入节点

7. 在链表尾部删除节点

8. 在链表头部删除节点

9. 查找链表中的指定节点

10. 在pos节点之前插入节点

11. 删除pos节点

小结

12. 释放链表

List.h

List.c


前言

在前几章实现了用C语言实现无哨兵位单向不循环链表,以及一些单链表的oj题

数据结构 ——— C语言实现无哨兵位单向不循环链表-CSDN博客

数据结构 ——— 单链表oj题:相交链表(链表的共节点)-CSDN博客

数据结构 ——— 单链表oj题:环状链表(判断链表是否带环)-CSDN博客 

接下来要学习的是带哨兵位双向循环链表的概念及其实现


无哨兵位单向不循环链表的缺陷

  1. 在实现 无哨兵位单向不循环链表 时,可以发现对其尾插节点的时间复杂度为:O(N) ;因为要从头节点遍历,找到原尾节点,才能进行插入节点
  2. 在查找当前节点的前一个节点时,也需要从头节点开始遍历才能查找到
  3. 且在没有哨兵位的情况下,在进行插入时,都需要判断链表的头节点是否为 NULL

所以给出了 带哨兵位双向循环链表,可以有效的解决以上问题


带哨兵位双向循环链表的概念

  1. 带哨兵位可以有效的避免对头节点为 NULL 时的判断,可以直接插入或者删除
  2. 双向的好处在于对当前节点的前一个节点或者后一个节点的查找时间复杂度为:O(1)
  3. 链表循环的好处在于对于尾插的时间复杂度为:O(1)

带哨兵位双向循环链表的结构

// 节点数据的类型
typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* prev; //指向前一个节点的指针
	
	LTDataType data; //节点的数据
	
	struct ListNode* next; //指向后一个节点的指针
}LNode;

以上的结构就是 带哨兵位双向循环链表 中每一个节点的结构
prev 指针指向前一个节点,next 指针指向后一个节点,这样就实现了双向的结构


带哨兵位双向循环链表逻辑结构示意图

哨兵位 head 节点的 prev 指向最后一个节点,next 指向第一个节点
这样就实现了带哨兵位循环的结构


实现带哨兵位双向循环链表的准备工作

和实现 无哨兵位单向不循环链表 一样,需要准备3个文件

test.c 文件:用来测试代码功能的完善性

List.h 文件:用来声明头文件和定义单链表的结构以及函数的声明

List.c 文件:用来实现单链表的功能函数


实现带哨兵位双向循环链表

1. 创建新节点

static LNode* BuyLTNode(LTDataType x)
{
	LNode* newnode = (LNode*)malloc(sizeof(LNode));

	// 判断是否开辟成功
	if (newnode == NULL)
	{
		perror("BuyLTNode fail");
		return NULL;
	}

	newnode->data = x;
    newnode->prev = NULL;
    newnode->next = NULL;

	return newnode;
}

 使用 malloc 函数动态开辟一个 newnode 节点,并判断是否开辟成功
将数据 x 存储到 newnode 的 data 中,最后返回 newnode 节点指针


2. 初始化哨兵位

LNode* LTInit()
{
	LNode* phead = BuyLTNode(-1);

	phead->prev = phead;
	phead->next = phead;

	return phead;
}

在函数内部创建哨兵位,最后再返回哨兵位的指针,这样做的好处是避免使用二级指针
为什么要将哨兵位的 prev 和 next 都指向自己呢?这个留作悬念,在尾插时会讲解到


3. 定义哨兵位指针

LNode* plist = LTInit();

直接利用初始化哨兵位的函数定义链表,这样就避免了使用二级指针
因为如果使用传址初始化哨兵位的话,就需要传递二级指针才能改变指针的指向
没有这样做是因为稳定函数的统一性,使函数都使用一级指针进行增删查改


4. 打印链表所有节点的数据

void LTPrint(LNode* phead)
{
	LNode* cur = phead->next;

	printf("head<->");

	while (cur != phead)
	{
		printf("%d<->", cur->data);
		cur = cur->next;
	}

	printf("head\n");
}

phead 的 next 也就是链表的第一个节点 cur 
因为链表是循环链表,那么尾节点的 next 就是哨兵位
所以 while 循环的判断条件是头节点 cur 不等于 哨兵位 phead
这样就实现了打印链表所有节点的数据


5. 在链表尾部插入节点

void LTPushBack(LNode* phead, LTDataType x)
{
	// 判断哨兵位的有效性
	assert(phead);

	LNode* newnode = BuyLTNode(x); //申请新节点
	LNode* tail = phead->prev; //找原尾节点

	// 原尾节点链接新尾节点
	tail->next = newnode;
	newnode->prev = tail;

	// 哨兵位链接新尾节点
	phead->prev = newnode;
	newnode->next = phead;
}

在链表尾部插入数据,那么就要先找到链表中原来的尾节点
且原来的尾节点 tail 就是哨兵位 phead 节点的 prev

先把原尾节点 tail 的 next 指向 新节点 newnode ,新节点 newnode 的 prev 指向原尾节点 tail ,这样 newnode 就成功链接成新的尾节点
最后再把哨兵位 phead 的 prev 指向新的尾节点 newnode ,新尾节点 newnode 的 next 指向 phead ,这样哨兵位就成功链接了新的尾节点

且当链表为空时,以上的代码逻辑同样适用,这就是为什么在初始化哨兵位时要将哨兵位的 prev 和 next 都指向自己的原因

测试代码:


6. 在链表头部插入节点

void LTPushFront(LNode* phead, LTDataType x)
{
	assert(phead);

	LNode* newnode = BuyLTNode(x); //申请新节点
	LNode* first = phead->next; //找原头节点

	// 原头节点链接新头节点
	newnode->next = first;
	first->prev = newnode;

	// 哨兵位链接新头节点
	phead->next = newnode;
	newnode->prev = phead;
}

先找到原来的头节点 first ,原来的头节点也就是 phead 的 next
再把原头节点和新头节点链接、哨兵位和新头节点链接,无论先后顺序都可以
这样就实现了在链表头部插入数据

以上的代码逻辑同样适用于当链表为空的情况

测试代码:


7. 在链表尾部删除节点

void LTPopBack(LNode* phead)
{
	assert(phead);

	// 当链表为空时
	if (phead->prev == phead)
	{
		printf("链表无节点可删除\n");
		return;
	}

	// 找到尾节点的前一个节点
	LNode* tailPrev = phead->prev->prev;

	// 删除(释放)尾节点
	free(phead->prev);

	// 哨兵位链接新尾节点
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

先找到尾节点的前一个节点,phead 的 prev 是尾节点,那么 phead 的 prev 的 prev 就是尾节点的前一个节点
再释放尾节点,最后再链接哨兵位和新的尾节点 tailPrev
这样就实现了在链表尾部删除节点

以上的代码逻辑在 链表只有一个节点 或者 链表为空 的时候同样适用

测试代码:


8. 在链表头部删除节点

void LTPopFront(LNode* phead)
{
	assert(phead);

	// 当链表为空时
	if (phead->next == phead)
	{
		printf("链表无节点可删除\n");
		return;
	}

	// 找到头节点的下一个节点
	LNode* firstNext = phead->next->next;

	// 释放头节点
	free(phead->next);

	// 哨兵位链接新的头节点
	phead->next = firstNext;
	firstNext->prev = phead;
}

先找到头节点的下一个节点,phead 的 next 就是头节点,那么 phead 的 next 的 next 就是头节点的下一个节点
再释放头节点,最后在把哨兵位和新的头节点链接
这样就实现了在链表头部删除节点

以上的代码逻辑在 链表只有一个节点 或者 链表为空 的时候同样适用

测试代码:


9. 查找链表中的指定节点

LNode* LTFind(LNode* phead, LTDataType x)
{
	assert(phead);

	LNode* cur = phead->next;

	while (cur != phead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}

	return NULL;
}

和 打印链表所有节点的数据 的逻辑差不多,也是需要遍历链表
当 cur 的 data 等于 x 时,cur 就是要查找的节点,返回 cur
当链表遍历完一遍还没有找到指定节点时,说明没有此节点,返回NULL即可 

以上的逻辑既有读也有写的功能,因为返回值是指定节点的指针,那么就可以进行修改
且在实现中间插入删除节点时,都要利用 LTFind 函数配合使用 


10. 在pos节点之前插入节点

void LTInsert(LNode* pos, LTDataType x)
{
	assert(pos);

	LNode* newnode = BuyLTNode(x);
	LNode* posPrev = pos->prev; //找到 pos 节点的前一个节点

	// posPrev 节点链接新节点
	posPrev->next = newnode;
	newnode->prev = posPrev;

	// 新节点链接 pos 节点
	newnode->next = pos;
	pos->prev = newnode;
}

优先判断 pos 指针的有效性,因为当 LTFind 函数没有查找到指定节点时,会返回 NULL
找到 pos 节点的前一个节点 posPrev
再将 posPrev 和 newnode 和 pos 节点各自链接,无论先后顺序都可以
这样就实现了在链表 pos 节点之前插入节点

以上代码逻辑同样适用于当链表只有一个节点的情况

测试代码:


11. 删除pos节点

void LTErase(LNode* pos)
{
	assert(pos);

	// 找到 pos 节点的前一个节点
	LNode* posPrev = pos->prev;

	// 找到 pos 节点的后一个节点
	LNode* posNext = pos->next;

	// 释放 pos 节点
	free(pos);

	// 链接 posPrev 和 posNext 节点
	posPrev->next = posNext;
	posNext->prev = posPrev;
}

同样优先判断 pos 节点的有效性
再找到 pos 节点的前一个节点 posPrev 和 pos 的后一个节点 posNext
最后释放 pos 节点,再链接 posPrev 和 posNext 节点即可
这样就实现了删除 pos 节点

以上代码逻辑同样适用于当链表只有一个节点的情况

测试代码:


小结

只要有了 LTFind 和 LTInsert 和 LTErase 这三个函数,以上的头插、尾插、头删、尾删这些函数都可以复用这三个函数,具体怎么复用我就不写了,望大家自己实现


12. 释放链表

void LTDestroy(LNode* phead) 
{
	assert(phead);

	LNode* cur = phead->next;
	LNode* next = NULL;

	while (cur != phead)
	{
		next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
}

从第一个节点开始释放,释放前要先存储下一个节点
最后再释放哨兵位 phead

自此,带哨兵位双向循环链表的基本函数都以实现,接下来就是做一些有关链表的oj题
感谢大家阅读,最后我会把定义和实现的所有代码文件展示在下面


List.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

// 节点数据的类型
typedef int LTDataType;

typedef struct ListNode
{
	struct ListNode* prev; //指向前一个节点的指针
	
	LTDataType data; //节点的数据
	
	struct ListNode* next; //指向后一个节点的指针
}LNode;

// 初始化哨兵位
LNode* LTInit();

// 打印
void LTPrint(LNode* phead);

// 尾插
void LTPushBack(LNode* phead, LTDataType x);

// 头插
void LTPushFront(LNode* phead, LTDataType x);

// 尾删
void LTPopBack(LNode* phead);

// 头删
void LTPopFront(LNode* phead);

// 查找
LNode* LTFind(LNode* phead, LTDataType x);
 
// pos节点之前插入
void LTInsert(LNode* pos, LTDataType x);

// 删除pos节点
void LTErase(LNode* pos);

// 释放链表
void LTDestroy(LNode* phead);

List.c 

#include"List.h"

// 创建新节点
static LNode* BuyLTNode(LTDataType x)
{
	LNode* newnode = (LNode*)malloc(sizeof(LNode));

	// 判断是否开辟成功
	if (newnode == NULL)
	{
		perror("BuyLTNode fail");
		return NULL;
	}

	newnode->data = x;
	newnode->prev = NULL;
	newnode->next = NULL;

	return newnode;
}

// 初始化哨兵位
LNode* LTInit()
{
	LNode* phead = BuyLTNode(-1);

	phead->prev = phead;
	phead->next = phead;

	return phead;
}

// 打印
void LTPrint(LNode* phead)
{
	assert(phead);

	LNode* cur = phead->next;

	printf("head<->");
	while (cur != phead)
	{
		printf("%d<->", cur->data);
		cur = cur->next;
	}
	printf("head\n\n");
}

// 尾插
void LTPushBack(LNode* phead, LTDataType x)
{
	// 判断哨兵位的有效性
	assert(phead);

	LNode* newnode = BuyLTNode(x); //申请新节点
	LNode* tail = phead->prev; //找尾节点

	// 原尾节点链接新尾节点
	tail->next = newnode;
	newnode->prev = tail;

	// 哨兵位链接新尾节点
	phead->prev = newnode;
	newnode->next = phead;
}

// 头插
void LTPushFront(LNode* phead, LTDataType x)
{
	assert(phead);

	LNode* newnode = BuyLTNode(x); //申请新节点
	LNode* first = phead->next; //找原头节点

	// 原头节点链接新头节点
	newnode->next = first;
	first->prev = newnode;

	// 哨兵位链接新头节点
	phead->next = newnode;
	newnode->prev = phead;
}

// 尾删
void LTPopBack(LNode* phead)
{
	assert(phead);

	// 当链表为空时
	if (phead->prev == phead)
	{
		printf("链表无节点可删除\n");
		return;
	}

	// 找到尾节点的前一个节点
	LNode* tailPrev = phead->prev->prev;

	// 删除(释放)尾节点
	free(phead->prev);

	// 哨兵位链接新尾节点
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

// 头删
void LTPopFront(LNode* phead)
{
	assert(phead);

	// 当链表为空时
	if (phead->next == phead)
	{
		printf("链表无节点可删除\n");
		return;
	}

	// 找到头节点的下一个节点
	LNode* firstNext = phead->next->next;

	// 释放头节点
	free(phead->next);

	// 哨兵位链接新的头节点
	phead->next = firstNext;
	firstNext->prev = phead;
}

// 查找
LNode* LTFind(LNode* phead, LTDataType x)
{
	assert(phead);

	LNode* cur = phead->next;

	while (cur != phead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}

	return NULL;
}

// pos节点之前插入
void LTInsert(LNode* pos, LTDataType x)
{
	assert(pos);

	LNode* newnode = BuyLTNode(x);
	LNode* posPrev = pos->prev; //找到 pos 节点的前一个节点

	// posPrev 节点链接新节点
	posPrev->next = newnode;
	newnode->prev = posPrev;

	// 新节点链接 pos 节点
	newnode->next = pos;
	pos->prev = newnode;
}

// 删除 pos 节点
void LTErase(LNode* pos)
{
	assert(pos);

	// 找到 pos 节点的前一个节点
	LNode* posPrev = pos->prev;

	// 找到 pos 节点的后一个节点
	LNode* posNext = pos->next;

	// 释放 pos 节点
	free(pos);

	// 链接 posPrev 和 posNext 节点
	posPrev->next = posNext;
	posNext->prev = posPrev;
}

// 释放链表
void LTDestroy(LNode* phead) 
{
	assert(phead);

	LNode* cur = phead->next;
	LNode* next = NULL;

	while (cur != phead)
	{
		next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
}

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

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

相关文章

【最新华为OD机试E卷-支持在线评测】考勤信息(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…

SCI论文快速排版:word模板一键复制样式和格式【重制版】

关注B站可以观看更多实战教学视频&#xff1a;hallo128的个人空间SCI论文快速排版&#xff1a;word模板一键复制样式和格式&#xff1a;视频操作视频重置版2【推荐】 SCI论文快速排版&#xff1a;word模板一键复制样式和格式【重制版】 模板与普通文档的区别 为了让读者更好地…

软考系统分析师知识点十:软件工程

前言 今年报考了11月份的软考高级&#xff1a;系统分析师。 考试时间为&#xff1a;11月9日。 倒计时&#xff1a;27天。 目标&#xff1a;优先应试&#xff0c;其次学习&#xff0c;再次实践。 复习计划第一阶段&#xff1a;扫平基础知识点&#xff0c;仅抽取有用信息&am…

苹果AI科学家研究证明基于LLM的模型存在缺陷 因为它们无法推理

苹果公司人工智能科学家的一篇新论文发现&#xff0c;基于大型语言模型的引擎&#xff08;如 Meta 和 OpenAI 的引擎&#xff09;仍然缺乏基本的推理能力。该小组提出了一个新的基准–GSM-Symbolic&#xff0c;以帮助其他人衡量各种大型语言模型&#xff08;LLM&#xff09;的推…

【C++贪心 DFS】2673. 使二叉树所有路径值相等的最小代价|1917

本文涉及知识点 C贪心 反证法 决策包容性 CDFS LeetCode2673. 使二叉树所有路径值相等的最小代价 给你一个整数 n 表示一棵 满二叉树 里面节点的数目&#xff0c;节点编号从 1 到 n 。根节点编号为 1 &#xff0c;树中每个非叶子节点 i 都有两个孩子&#xff0c;分别是左孩子…

QD1-P7 HTML 容器和布局标签(div、span)

本节学习&#xff1a;div 和 span 标签。 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p7 ‍ 一、div 标签 用途 ​<div>​ 标签在 HTML 中是一个通用容器&#xff0c;用于将 HTML 文档中的内容分组并在文档中划分区域。<div>​ ​元素本身不具有特定的含…

深入探索Spring Cloud Gateway:微服务网关的最佳实践

优质博文&#xff1a;IT-BLOG-CN Spring Cloud Gateway作为Spring Cloud框架的第二代网关&#xff0c;在功能上要比Zuul更加的强大&#xff0c;性能也更好。随着Spring Cloud的版本迭代&#xff0c;Spring Cloud官方有打算弃用Zuul的意思。在笔者调用了Spring Cloud Gateway的…

前端方案:根据链接生成二维码

前言&#xff1a; 虽然在很多时候&#xff0c;生成二维码的操作都是由后端进行操作。但是在某些特定的场景里&#xff0c;难免会需要前端来完成链接生成二维码的操作&#xff0c;在这里我们提供一个插件来完成&#xff0c;这个插件就是qrcode。 官方地址 安装&#xff1a; …

Enemy Golem 卡通石头人怪物模型带骨骼动画动作

包含9个动画。 信息: -模型有9.450个涵洞。 -矿脉x 4 -纹理:彩色、普通、蒙版、AO、发射型(2048x2048尺寸) 下载:​​Unity资源商店链接资源下载链接 效果图:

【多模态论文阅读系列二】— MiniCPM-V

校招/实习简历修改、模拟面试欢迎私信《MiniCPM-V: A GPT-4V Level MLLM on Your Phone》 在本节中&#xff0c;我们介绍了MiniCPM-V的模型架构&#xff0c;概述了其总体结构和自适应高分辨率视觉编码方法。MiniCPM-V系列的设计理念是在性能和效率之间实现良好的平衡&#xff0…

默语是谁?

默语是谁&#xff1f; 大家好&#xff0c;我是 默语&#xff0c;别名默语博主&#xff0c;擅长的技术领域包括Java、运维和人工智能。我的技术背景扎实&#xff0c;涵盖了从后端开发到前端框架的各个方面&#xff0c;特别是在Java 性能优化、多线程编程、算法优化等领域有深厚…

一文了解 Linux 系统的文件权限管理

文章目录 引入Linux文件权限模型查看文件权限权限信息解析修改文件权限符号模式八进制数字模式 引入 在Linux操作系统中&#xff0c;我们想查看我们对文件拥有哪些权限时&#xff0c;可以在终端键入ls -l或ll命令&#xff0c;终端会输出当前路径下的文件信息&#xff0c;如文件…

vue3集成electron

安装说明 vue集成electron时&#xff0c;会用到两个依赖。分别是electron和electron-builder&#xff0c;前者是开发环境下使用&#xff0c;后者是打包部署时使用。安装时&#xff0c;可在线安装也可离线安装。所谓离线安装就是自己下载好用到的包&#xff0c;然后放到指定目录…

Spring Boot知识管理系统:安全与合规性

4系统概要设计 4.1概述 本系统采用B/S结构(Browser/Server,浏览器/服务器结构)和基于Web服务两种模式&#xff0c;是一个适用于Internet环境下的模型结构。只要用户能连上Internet,便可以在任何时间、任何地点使用。系统工作原理图如图4-1所示&#xff1a; 图4-1系统工作原理…

HI3516DV500 相机部分架构初探

Hi3516DV500 是一颗面向视觉行业推出的高清智能 Soc。该芯片最高支持 2 路 sensor 输入&#xff0c;支持最高 5M30fps 的 ISP 图像处理能力&#xff0c;支持 2F WDR、多级降噪、六轴防 抖、多光谱融合等多种传统图像增强和处理算法&#xff0c;支持通过 AI 算法对输入图像进行实…

Mysql(3)—数据库相关概念及工作原理

一、数据库相关概念 ​ 数据库&#xff08;Database, DB&#xff09; &#xff1a; 数据库是一个以某种有组织的方式存储的数据集合。它通常包括一个或多个不同的主题领域或用途的数据表。 数据库管理系统&#xff08;Database Management System, DBMS&#xff09; &#xf…

树莓派应用--AI项目实战篇来啦-15.SSD Mobilenet V3目标检测

1. Mobilenet 介绍 Mobilenet 是一种专为移动和嵌入式视觉应用而设计的卷积神经网络。它们不使用标准的卷积层&#xff0c;而是基于使用深度可分离卷积的简化架构&#xff0c;使用这种架构&#xff0c;我们可以为移动和嵌入式设备&#xff08;例如&#xff1a;树莓派&#xff0…

Navicat 关于SQLserver的连接问题

1、如果出以下问题&#xff0c;就需要安装驱动程序&#xff0c;如下图&#xff1a; 2、在Navicat的根目录下有一个驱动安装文件&#xff0c;安装后就可以连接上了.

Cisco ACI常见问题FAQ科普

这里有个思科的官方链接&#xff0c;不过里面很多是商务说辞&#xff0c;也就是吹牛&#xff0c;仅做为参考。 https://www.cisco.com/c/dam/global/en_sg/solutions/data-center-virtualization/application-centric-infrastructure/insieme_faq.pdf 下面是我自己的理解 0 …

Windows 安装Redis(图文详解)

Windows 安装Redis&#xff08;图文详解&#xff09; Redis是什么数据库&#xff1f; Remote Dictionary Server(Redis) 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库&#xff0c;并提供多种语…