手撕数据结构 —— 带头双向循环链表(C语言讲解)

目录

0.前言

1.什么是带头双向循环链表

理解带头

​编辑

理解双向

理解循环

2.带头双向循环链表的实现

List.h文件中接口总览

具体实现 

结点的定义

申请结点

初始化

打印链表

尾插

尾删

头插

头删

​编辑​编辑

获取大小

查找 

在指定位置前插入

​编辑

删除指定位置的值


0.前言

本篇文章旨在讲解带头双向循环链表的实现,如果读者并不了解链表的基础知识,推荐阅读 —— 手撕数据结构 —— 单链表(C语言讲解)

1.什么是带头双向循环链表

理解带头

什么是带头:带头的意思是链表多申请一个结点放在链表的起始位置,该结点并不存储有效元素。上图中的head结点就是头结点,该结点也往往称之为哨兵位。

带头的作用:该结点的主要作用是为了方便实现链表和操作链表。主要体现在两个方面:1、提供统一的操作方式。2、避免二级指针的使用。

  • 提供统一的操作方式。因为操作没有头结点的链表的时候,往往需要记录当前结点的上一个结点,但是第一个结点是没有上一个结点的,就需要特殊处理,设置哨兵位的头结点可以对所有存储有效元素的结点提供统一的操作方式。
  • 避免二级指针的使用。在实现单链表的时候,我们有时候需要改变的是结构体指针,这个时候就需要将参数设置为二级指针。有的时候需要改变的是结构体当中的成员,这个时候需要将参数设置为一级指针。这样不方便理解和实现,引入哨兵位的头结点之后,我们不需要改变结构体指针,避免了二级指针的使用。

关于带头的好处读者先了解,后续实现会深有体会。

理解双向

什么是双向:双向就是指结点中会包含两个指针域,一个指针域记录上一个结点的地址,一个指针域记录下一个结点的地址。 不像单链表,只是记录了下一个结点的地址。

双向的作用:在链表的实现中,往往需要使用当前结点的上一个结点(比如在某个位置之前插入节点)。对于单链表来说,只能在寻找指定节点的时候记录上一个结点,操作比较复杂,而双向链表中记录了上一个结点的地址,直接就能找到上一个结点,操作简单。

理解循环

什么是循环:循环的意思就是链表形成回路,最后一个结点的指针域指向第一个节点。

循环的作用:在操作链表的时候,我们往往知道头结点,需要寻找尾结点。单链表只能遍历链表去找,时间复杂度尾O(N);双向循环链表的头结点中记录了尾结点的地址,直接就能找到,时间复杂度为O(1),在需要找尾结点的操作中,大大提高了效率。

可以看出,带头双向循环链表是对单链表的升级,是一种提高链表效率的结构,是一种十分优秀的结构。

2.带头双向循环链表的实现

实现带头双向循环链表,我们主要实现List.h文件和List.c文件,List.h文件中存放声明,List.c文件中存放定义。

List.h文件中接口总览

具体实现 

结点的定义

带头双向循环链表的结点中需要记录数据、上一个结点以及下一个结点。

  • data用来记录有效数据。
  • prev记录上一个结点的地址。
  • next记录下一个结点的地址。

申请结点

我们使用malloc函数申请结点。

  • 申请的数据的类型是自定义的结点类型。
  • data设置为指定的值。
  • next指针和prev指针设置为空。

初始化

初始化就是申请一个哨兵位的头结点,该节点的prev和next都指向自己。

  • 初始化的时候,我们申请的结点是在堆空间上申请的,堆上申请的变量除非手动释放,否则一直存在。
  • 最后返回指向这块空间的指针变量。

打印链表

打印链表只需要遍历输出即可。

  • 注意phead的值不能为空,使用断言暴力判断。
  • cur指向要打印的元素,从哨兵位的下一个结点开始打印,当 cur == phead 的时候,说明所有的结点都打印了,退出循环,打印结束。

注意,所有涉及 LTNode* 类型的指针都不能为空! 

尾插

在链表的末尾插入结点,寻找尾结点的时候,直接一步到位,不需要遍历寻找。找到尾结点之后,依次和前一个结点连接,和后一个结点连接即可。

尾删

删除尾结点的时候,首先要找到尾结点和尾结点的前一个结点;释放尾结点后,将新的尾结点和哨兵位连接。

头插

头插是在哨兵位的后面,第一个有效结点的前面插入数据。需要注意的是:

  • 该代码中并没有记录phead的下一个结点,连接的时候需要从后往前连接。如果记录了phead的下一个结点,那么先连接和后连接哪个结点都可以。

头删

在头部删除数据时,我们删除的是哨兵位后面的第一个结点。

  • 依次记录哨兵位后面的第一个结点和第二个结点,删除的时候,只需要改变对于指针的指向即可。

获取大小

和打印链表的方法是一样的,只不过遍历的时候记录结点的个数并返回即可。

查找 

查找和打印差不多,通过遍历进行查找,当结点的数据等于指定元素时,返回该节点的地址即可,没找到返回NULL。

在指定位置前插入

我们先记录pos位置的前一个位置,然后连接即可。

删除指定位置的值

删除指定位置的结点,我们可以先记录指定位置的前一个结点和后一个结点,释放指定位置的节点,然后连接posPrev和posNext即可。

3.完整代码附录

List.h

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

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;                     //指向后一个结点 
	struct ListNode* prev;                     //指向前一个结点 
	LTDataType data;                           //存储数据 
}LTNode;


LTNode* BuyLTNode(LTDataType x);               //申请结点 

LTNode* LTInit();                              //初始化链表 

void LTPrint(LTNode* phead);                   //打印链表 

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

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

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

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

int LTSize(LTNode* phead);                     //获取链表大小 

LTNode* LTFind(LTNode* phead, LTDataType x);   //查找指定结点 

void LTInsert(LTNode* pos, LTDataType x);      //在指定结点位置插入 

void LTErase(LTNode* pos);                     //删除指定结点 

List.c

#include"List.h"

// 申请结点 
LTNode* BuyLTNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

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

	return node;
}

// 初始化 
LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

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

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

// 尾插 
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;      // 直接找到尾结点 
	LTNode* newnode = BuyLTNode(x);  // 申请一个新结点 

	// 连接newnode的tail 
	newnode->prev = tail;
	tail->next = newnode;
	// 连接newnode和phead 
	newnode->next = phead;
	phead->prev = newnode;
}

// 尾删 
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);  // 保证链表中有数据才删 

	LTNode* tail = phead->prev;    // 找到尾结点 
	LTNode* tailPrev = tail->prev; // 找到尾结点的前一个结点 
	free(tail);                    // 释放尾结点 
	
	// 将新的尾结点和哨兵位连接 
	tailPrev->next = phead;
	phead->prev = tailPrev;
}

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

	LTNode* newnode = BuyLTNode(x); // 申请新结点 
	
	// 连接新结点和哨兵位的下一个结点 
	newnode->next = phead->next;
	phead->next->prev = newnode;
	
	// 连接哨兵位和新结点 
	phead->next = newnode;
	newnode->prev = phead;
}

// 头删 
void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead); // 确保有存储数据的结点再删除 

	LTNode* first = phead->next;  // 记录第一个存储有效数据的结点 
	LTNode* second = first->next; // 记录第二个存储有效数据的结点 

	free(first);                  // 释放第一个存储有效数据的结点 

	// 将哨兵位和第二个存储有效数据的结点连接 
	phead->next = second;
	second->prev = phead;
}


// 查找 
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	
	LTNode* cur = phead->next; // 记录第一个存储有效元素的结点 
	while(cur != phead)
	{
		if(cur->data == x)
			return cur;
	}
	
	return NULL;
}

// 在指定位置之前插入数据 
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);                    // 指定的位置不能为空 

	LTNode* posPrev = pos->prev;    // 记录指定位置的前一个位置 
	
	LTNode* newnode = BuyLTNode(x); // 申请新结点 

	// 将新结点链接进链表中 
	posPrev->next = newnode;
	newnode->prev = posPrev;
	newnode->next = pos;
	pos->prev = newnode;
}

// 删除指定位置的值 
void LTErase(LTNode* pos)
{
	assert(pos);                  // 指定位置不能为空 
	
	LTNode* posPrev = pos->prev;  // 记录pos的前一个位置 
	LTNode* posNext = pos->next;  // 记录pos的后一个位置 

	free(pos);                    // 释放pos指向的节点 

	// 连接posPrev和posNext 
	posPrev->next = posNext;
	posNext->prev = posPrev;
}

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

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

相关文章

数据结构--线性表双向链表的实现

目录 思路设计 总体思维导图 插入部分 头插法尾插法 任意位置插入 删除部分 头结点 尾节点 中间节点 只有头结点且删除的就是头结点 ​编辑 清空链表部分 遍历清空链表的所有节点 不遍历清空 各部分代码 Main部分 MyListedList部分 IndexOutOfException部分 …

YOLO11改进 | 注意力机制 | 结合静态和动态上下文信息的注意力机制

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 上下文Transformer&#xff08;CoT&…

WPF中的布局

布局原则 1、不显式设置元素大小。 2、不使用绝对定位。 元素应该根据容器的内容来进行排列。绝对定位在开发前期会带来一些便捷&#xff0c;但扩展性比较差。一旦显示器尺寸或分辨率发生改变&#xff0c;界面的显示效果可能会达不到预期的效果。 3、布局容器可以嵌套使用 常…

【Axure原型分享】标签管理列表

今天和大家分享通过标签管理列表的原型模板&#xff0c;包括增删改查搜索筛选排序分页翻页等效果&#xff0c;这个模板是用中继器制作的&#xff0c;所以使用也很方便&#xff0c;初始数据我们只要在中继器表格里填写即可&#xff0c;具体效果可以观看下方视频或者打开预览地址…

【经管】上市公司供应链金融数据(1990-2023年)

上市公司供应链金融是指上市公司利用其产业链核心地位&#xff0c;通过整合金融资源&#xff0c;为供应链上下游企业提供包括融资、结算、风险管理等在内的综合金融服务。为了衡量上市公司的供应链金融水平&#xff0c;参考周兰等&#xff08;2022&#xff09;的研究方法&#…

【C++入门篇 - 3】:从C到C++第二篇

文章目录 从C到C第二篇new和delete命名空间命名空间的访问 cin和coutstring的基本使用 从C到C第二篇 new和delete 在C中用来向系统申请堆区的内存空间 New的作用相当于C语言中的malloc Delete的作用相当于C语言中的free 注意&#xff1a;在C语言中&#xff0c;如果内存不够…

IBM Flex System服务器硬件监控指标解读

随着企业IT架构的日益复杂&#xff0c;服务器的稳定运行对于保障业务连续性至关重要。IBM Flex System作为一款模块化、可扩展的服务器解决方案&#xff0c;广泛应用于各种企业级环境中。为了确保IBM Flex System服务器的稳定运行&#xff0c;监控易作为一款专业的IT基础设施监…

git维护【.gitignore文件】

在工程下添加 .gitignore 文件【git忽略文件】 *.class .idea *.iml *.jar /*/target/更多&#xff1a; # Compiled class file *.class# Log file *.log *.imi *.lst# BlueJ files *.ctxt# Mobile Tools for Java (J2ME) .mtj.tmp/# Package Files # *.jar *.war *.nar *.ea…

【MySQL 保姆级教学】数据库基础(重点)(2)

目录 1. 什么是数据库1.1 数据库的定义1.2 mysql 和 mysqld1.3 文件和数据库 2. 数据库的分类3. 连接数据库3.1 数据库的安装3.2 连接服务器&#xff08;数据库&#xff09;3.3 服务器 数据库 表 三者的关系 4. 数据库-表 和目录-文件 的关系5. MySQL 框架6. SQL 分类7. 储存引…

DDoS攻击快速增长,如何在抗ddos防护中获得主动?

当下DDoS攻击规模不断突破上限。前段时间&#xff0c;中国首款3A《黑神话&#xff1a;悟空》也在一夜之内遭受到28万次攻击DDoS攻击&#xff0c;严重影响到全球玩家的游戏体验。Gcore发布的数据也显示了 DDoS攻击令人担忧的趋势&#xff0c;尤其是峰值攻击已增加到了令人震惊的…

CNN-GRU时序预测 | MATLAB实现CNN-GRU卷积门控循环单元时间序列预测

时序预测 | MATLAB实CNN-GRU卷积门控循环单元时间序列预测 目录 时序预测 | MATLAB实CNN-GRU卷积门控循环单元时间序列预测预测效果基本介绍模型描述程序设计参考资料预测效果 基本介绍 本次运行测试环境MATLAB2020b 提出了一种基于卷积神经网络(Convolutional Neural Network…

生成式专题的第一节课---GAN图像生成

一、GAN的起源与发展 1.GAN的起源 GAN &#xff08;生成式对抗网络&#xff09;诞生于 2014 年&#xff0c;由 Ian Goodfellow 提出&#xff0c;是用于生成数据的深度学习模型&#xff0c;创新点是对抗性训练&#xff0c;即生成器与判别器的竞争关系&#xff0c;为图像生成、…

【网络安全】利用XSS、OAuth配置错误实现token窃取及账户接管 (ATO)

未经许可,不得转载。 文章目录 正文正文 目标:target.com 在子域sub1.target.com上,我发现了一个XSS漏洞。由于针对该子域的漏洞悬赏较低,我希望通过此漏洞将攻击升级至app.target.com,因为该子域的悬赏更高。 分析认证机制后,我发现: sub1.target.com:使用基于Cook…

DBA | 如何将 .mdf 与 .ldf 的数据库文件导入到SQL Server 数据库中?

[ 知识是人生的灯塔&#xff0c;只有不断学习&#xff0c;才能照亮前行的道路 ] 原文链接&#xff1a;DBA | 如何将 .mdf 与 .ldf 的数据库文件导入到SQL Server 数据库中? 如何将 (.mdf) 和 (.ldf) 的SQL Server 数据库文件导入到当前数据库中? Step 1.登录到 Sql Server 服…

Springboot——使用poi实现excel动态图片导入解析

文章目录 前言依赖引入导入实现方式一方式二 导出参考 前言 最近要实现一个导入导出的功能点&#xff0c;需要能将带图片的列表数据导出到excel中&#xff0c;且可以导入带图片的excel列表数据。 考虑到低代码平台的表头与数据的不确定性&#xff0c;技术框架上暂定使用Apach…

线性代数在大一计算机课程中的重要性

线性代数在大一计算机课程中的重要性 线性代数是一门研究向量空间、矩阵运算和线性变换的数学学科&#xff0c;在计算机科学中有着广泛的应用。大一的计算机课程中&#xff0c;线性代数的学习为学生们掌握许多计算机领域的关键概念打下了坚实的基础。本文将介绍线性代数的基本…

C++一个很好的计时方法

C一个很好的计时方法 //记时LARGE_INTEGER t1;LARGE_INTEGER t2;LARGE_INTEGER f;QueryPerformanceFrequency(&f);QueryPerformanceCounter(&t1);Sleep(100);QueryPerformanceCounter(&t2);double time;time (double)(t2.QuadPart-t1.QuadPart)/(double)f.QuadPar…

【Flutter】合并多个流Stream

1.说明 无意间发现了一个好用的库rxdart&#xff0c;它为 Dart 的 Stream 添加了额外的功能。 2.功能 &#xff08;1&#xff09;合并多个流Stream 借助Rx.combineLatest2()合并两个流stream1和stream2。 注意&#xff1a;如果dart文件中同时使用了getx&#xff0c;需要隐…

UE4 材质学习笔记03(翻书(Flipbook)动画/环境混合)

一.FlipBook Animation 如果你想让游戏以每秒30帧的速度运行&#xff0c;所有内容都必须在33毫秒内渲染出来&#xff0c; 如果你想让游戏以每秒60帧的速度运行的话&#xff0c;必须在16毫秒内。 所以当一个效果需要很多细节的时候&#xff0c;往往会离线创建它&#xff0c;然…