单链表你别再找我了,我怕双向链表误会

目录

带头双向循环链表的创建和初始化

创建一个新的结点(方便复用)

链表判空

链表打印

链表尾插

链表尾删

链表头插

链表头删

任意插入

任意删除

链表查找

链表销毁

完整代码


😎前言

  • 之前我们讲了结构最简单,实现起来却最为复杂的单链表——不带头单向不循环链表,但其往往不会被运用到存储数据当中,而是作为一些数据结构的子结构(如哈希表)。
  • 而今天我们要学的则是听起来,看起来结构最为复杂,但是实施起来却最为简单的带头双向循环链表。

带头双向循环链表的创建和初始化

首先我们需要一个哨兵位,因为在单链表的学习当中我们发现,如果没有哨兵位,当链表为空的时候插入结点就要改变头结点,这时候就要传二级指针或者返回值,也要和其他情况区分,不能一套代码解决所有情况。但如果带哨兵位头节点的话,二级指针和返回值都不需要用到,并且头删头插尾插尾删都特别的方便,它的优势处,下面会一一体现。

结点的定义也和单链表不同,多了一个指针指向前一个结点

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

 此时的head就是我们的哨兵位,但是不存储有效数据。如果链表为空,则head的prev以及next都指针都指向自己。

基于以上思路,我们可以写出创建一个哨兵位的代码了,也就是双向链表的建立。

ListNode* ListCreate()
{
	ListNode*dummmyhead=(ListNode*)malloc(sizeof(ListNode));
	dummmyhead->next = dummmyhead;
	dummmyhead->prev = dummmyhead;
	return dummmyhead;
}

创建一个新的结点(方便复用)

  • 在后续的插入接口中,要经常创建一个新的结点进行插入,所以在开头我们可以先将其制作成一个接口,方便代码的复用,这是很重要的工程项目思维。
  • 创建新的结点,其实就是向内存申请一块空间,用malloc即可
  • 防止野指针问题,每次创建的新结点的prev和next指针我们都置空。之后将这个节点的地址返回即可,因为该节点的空间是在堆上的,函数栈帧销毁不会影响这段空间。

按照以上思路,我们可以写出以下函数接口

//创建一个新节点,方便复用
ListNode* BuyNode(int x)
{
	ListNode* newNode=(ListNode*)malloc(sizeof(ListNode));
	if (newNode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

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

	return newNode;
}

链表判空

  • 和单链表一样,当链表为空了,就不可以再进行删除操作了,因此在后面的删除接口中判空是要用到的,我们还是可以先提前将其封装成一个接口,这样可以提高代码的可读性
  • 这里需要运用到bool值,空返回true;不空返回false

上文创建头结点的时候提到了哨兵位的prev和next指针都指向自己,所以如果链表为空,那么dummyhead->next=dummyhead

根据以上思路我们可以写出以下函数接口

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

	return pHead->next == pHead;
}

链表打印

  • 打印是为了观察以后的各个接口是否正确,比起调试来说更简单直观一些
  • 将写的代码可视化了,增加了我们的一点成就感
  • 遍历除了dummyhead以外的结点,直到重新指向dummyhead的时候结束
  • 自己可以设计一些箭头,看起来更加好看一点

下面为函数接口实现:

// 双向链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->next;
	printf("dummyhead<-> ");
	while (cur != pHead)
	{
		printf("%d <-> ",cur->data);
		cur = cur->next;
	}
	printf("dummyhead ");
}

链表尾插

  • 单链表中我们要从前往后找到最后一个结点,然后再插入,时间复杂度为O(N)。
  • 而双向链表实现起来十分简单,因为哨兵位的前一个结点就是链表的尾了,这样时间复杂度直接降为O(1),效率提升的让人不敢相信。
  • 如果此时链表中没有有效的节点,那么此时直接得到一个节点与头节点连接即可,不需要考虑单链表章节是否要更新头指针的情况,这也体现了带头的优处。

下面为函数接口实现:

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* newNode=BuyNode(x);
	ListNode* tail = pHead->prev;

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

}

链表尾删

  • 尾删就是要删除最后一个结点,所以我们要找到最后一个结点的前一个结点(dummyhead->prev->prev),将其与头结点相连,然后free掉尾结点。
  • 如果此时没有节点,这是判空的作用就来了,没有节点当然就是不给删咯,直接assert断言暴打。

  • 即使链表只有一个结点,也没有任何影响,大家可以自己手动画图一下,十分好理解

以下为函数实现接口:

void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListNode* tail = pHead->prev;
	ListNode* prev = tail->prev;
	
	pHead->prev = prev;
	prev->next = pHead;
	free(tail);
}

链表头插

  • 头插,即头结点和新结点相连,再将新结点与原来的头结点相连,这样新结点就成为了新的头结点啦!
  • 为了避免改变结点指向后写出错误代码,我们在修改指向前可以先定义几个变量,也增加代码可读性

以下为函数接口实现:

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* head = pHead->next;
	ListNode* newNode = BuyNode(x);

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

}

链表头删

  • 头删,即将哨兵位的下一个结点删除,将哨兵位和头结点的下一个结点连接起来。
  • 依然为了防止将链表指向修改后导致的代码书写错误,我们可以先定义变量记录头结点的下一个指针
  • 如果此时没有节点,这是判空的作用就来了,没有节点当然就是不给删咯,直接assert断言暴打。
  • 即使链表只有一个结点,也没有任何影响,大家可以自己手动画图一下,十分好理解
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListNode* head = pHead->next;
	ListNode* next = head->next;
	pHead->next = next;
	next->prev = pHead;
	free(head);
}

任意插入

  • 这里的任意插入,是将你要插入的节点插入到你想要插入的位置的前面。
  • 同样的,这里需要传递一个节点的指针(pos)代表你要插入的位置
  • 既然是在想插入的位置的前面插入,那么这里就需要存放一下该位置的前一个节点的地址,然后进行连接即可

以下为相关代码的实现:

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* newNode = BuyNode(x);
	ListNode* prev = pos->prev;

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

任意删除

  • 任意删除,是删除pos位置,这个pos就有序列表是你指定要删除的那个节点的地址。

  • 既然是删除pos位置,就需要存放一下pos的前一个节点的地址和pos的下一个节点的地址,以便于连接。最后释放pos位置的节点即可。

下面为函数接口实现:

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	// 判断pos的有效性
	assert(pos);

	// 存放pos的下一个节点的地址	
	ListNode* next = pos->_next;
	// 存放pos的前一个节点的地址
	ListNode* prev = pos->_prev;
	// 删除(释放)pos
	free(pos);
	// 连接
	prev->_next = next;
	next->_prev = prev;
}

链表查找

  • 查找也就是从头结点往后一个一个看是否与值匹配,找到的话就返回首个匹配的节点位置(也可以自己修改)
  • 单链表是到NULL结束循环,而这里则是当指针重新指向dummyhead的时候,意味着所有结点都已经查找完了,此时返回NULL

下面为函数接口实现:

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->data == x)
		return cur;
		else cur = cur->next;
	}
	return NULL;
}

链表销毁

  • 因为各个结点都是malloc出来的,所以如果不在使用后销毁可能会导致内存泄漏,所以我们要养成良好习惯,即使很多编译器已经很明智了
  • 销毁的对象包括dummyhead
  • 需要额外记录被销毁的结点的下一个结点

下面为函数接口实现:

// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(pHead);
}

总结

至此,常用的接口我们已经实现的差不多了,我们会发现,双向链表正是靠其复杂的结构从而使得代码实现起来非常方便,不得不佩服想出这种结构的人。

相对于单链表,这种带头双向循环链表简直是六边形链表,任何方面都碾压了单链表。

单链表别再来找我了,我怕双向链表误会👉👈

以下是完整代码,有需求的小伙伴们可以拿走哦

完整代码

#include"List.h"

// 创建返回链表的头结点.
ListNode* ListCreate()
{
	ListNode*dummmyhead=(ListNode*)malloc(sizeof(ListNode));
	dummmyhead->next = dummmyhead;
	dummmyhead->prev = dummmyhead;
	return dummmyhead;
}
//创建一个新节点,方便复用
ListNode* BuyNode(int x)
{
	ListNode* newNode=(ListNode*)malloc(sizeof(ListNode));
	if (newNode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

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

	return newNode;
}
// 双向链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->next;
	printf("dummyhead<-> ");
	while (cur != pHead)
	{
		printf("%d <-> ",cur->data);
		cur = cur->next;
	}
	printf("dummyhead ");
}
//判断链表是否为空
bool ListEmpty(ListNode* pHead)
{
	assert(pHead);

	return pHead->next == pHead;
}
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* newNode=BuyNode(x);
	ListNode* tail = pHead->prev;

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

}
// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListNode* tail = pHead->prev;
	ListNode* prev = tail->prev;
	
	pHead->prev = prev;
	prev->next = pHead;
	free(tail);
}
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* head = pHead->next;
	ListNode* newNode = BuyNode(x);

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

}
// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(!ListEmpty(pHead));

	ListNode* head = pHead->next;
	ListNode* next = head->next;
	pHead->next = next;
	next->prev = pHead;
	free(head);
}
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);

	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		if (cur->data == x)
		return cur;
		else cur = cur->next;
	}
	return NULL;
}
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* newNode = BuyNode(x);
	ListNode* prev = pos->prev;

	prev->next = newNode;
	newNode->prev = prev;
	newNode->next = pos;
	pos->prev = newNode;
}
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	prev->next = next;
	next->prev = prev;
	free(pos);

}
// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	assert(pHead);

	ListNode* cur = pHead->next;
	while (cur != pHead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(pHead);
}

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

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

相关文章

Spring —— Spring Boot 配置文件

JavaEE传送门 JavaEE Spring —— Bean 作用域和生命周期 Spring —— Spring Boot 创建和使用 目录 Spring Boot 配置文件Spring Boot 配置文件格式properties配置文件properties 基本语法properties 缺点 yml 配置文件yml 基本语法yml 配置不同类型数据及 nullyml 配置对象…

方案设计——食物测温仪方案

食物测温仪&#xff0c;在食物烹饪时&#xff0c;温度和时间至关重要&#xff0c;所以食物测温仪孕育而生&#xff0c;当用户使用时只需将食物测温仪的探头插入食物中&#xff0c;即刻能得到当前食物温度数据&#xff0c;不必用经验判断。做为一款食物测温仪&#xff0c;运用场…

Extra Finance 主网测试版上线,完成任务领空投

DeFi 的广泛应用将上一轮牛市推向顶峰&#xff0c;也让区块链具有了更多的拓展性。经过熊市的洗礼&#xff0c;DeFi 应用开始升级和优化&#xff0c;并且衍生出更多更加具有实用性和创新性的新产品。DeFi 已经成为区块链的基础设施&#xff0c;为更多的应用和创新提供帮助。下一…

“AI孙燕姿”们侵了谁的权?

“2003年大火的歌手&#xff1a;孙燕姿&#xff1b;2023年大火的歌手&#xff1a;AI孙燕姿”。在B站&#xff0c;这条评论获赞2800多&#xff0c;而被网友们集体点赞的是用AI克隆孙燕姿声音后演唱其他歌曲的视频。 截止目前&#xff0c;Up主们打造的“AI孙燕姿”已翻唱了百余首…

cam_lidar_calibration标定速腾激光雷达和单目相机外参

目录 一、资源链接二、代码测试2.1安装依赖2.2代码下载和修改2.2.1 optimiser.h文件2.2.2 feature_extractor.h文件 2.3编译代码2.4测试数据集2.4.1迭代计算2.4.2查看校准结果 三、标定自己激光雷达和相机3.1修改代码3.1.1camera_info.yaml配置文件3.1.2params.yaml配置文件3.1…

【Linux】Linux编辑神器vim的使用

目录 一、Vim的基本概念 二、Vim的基本操作 1、进入vim 2、正常模式切换至插入模式 3、插入模式切换至正常模式 4、正常模式切换至底行模式 5、退出Vim编辑器 三、Vim正常模式命令集 1、移动光标 2、删除文字 3、复制 4、替换 5、撤销 四、Vim底行模式命令集 1、列出行号 2、光…

Spring MVC:常用参数(注解)的使用和参数绑定的验证

Spring MVC&#xff1a;常用参数&#xff08;注解&#xff09;的使用和参数绑定的验证 一、学习资源二、基础源码三、实验结果3.1 Spring MVC常用参数Controller和RequestMappingRequestMappingRequestParamPathVariableCookie ValueRequestHeader 3.2 Spring MVC参数绑定3.2.1…

JavaScript实现贪吃蛇小游戏(网页单机版)

文章目录 项目地址项目介绍游戏开始游戏暂停游戏模式游戏死亡重新开始 结尾 今天使用 JavaScript 实现了一个网页版的贪吃蛇小游戏。 项目地址 Github: https://github.com/herenpeng/snakeGitee: https://gitee.com/herenpeng/snake线上体验&#xff1a;https://herenpeng.g…

在线未注册域名批量查询-域名注册批量查询

域名批量注册查询 域名批量注册查询是一种工具&#xff0c;可以帮助用户批量查询并注册多个域名。这种工具通常被域名管理者、品牌专家、互联网营销人员等使用。 以下是域名批量注册查询工具的优点&#xff1a; 提高效率&#xff1a;与手动单独注册域名相比&#xff0c;域名批…

计算机网络实验(ensp)-实验1:初识eNSP仿真软件

目录 实验报告&#xff1a; 实验操作 1.建立网络拓扑图并开启设备 2.配置路由器 1.输入命名&#xff1a;sys 从用户视图切换到系统视图 2.输入命名&#xff1a;sysname 姓名 修改路由器名字 3.输入命名&#xff1a;interface g0/0/0 进入端口视图g0…

如何学习web前端开发?这样学前端事半功倍,能救一个是一个!

非常理解想要自学前端的伙伴&#xff0c;因为好程序员的学员一开始也是自学插画的&#xff0c;很多同学&#xff0c;自学到最后真的非常枯燥乏味&#xff0c;且走了很多弯路。小源想着能帮一把是一把的原则&#xff0c;这两天整理了一份前端的高效学习路线&#xff0c;想学web前…

Redis 学习笔记

一、简介 1、纯内存操作&#xff08;理解成容量就是内容条&#xff09; 2、作为缓存使用&#xff08;因为内存条操作&#xff0c;比磁盘速度快&#xff09; 二、 常见命令 类型命令string set、get、mset、mget、setrange、getrange、 incr、decr、incrby、decrby、incrbyfl…

基于Python3的tkinter Text文本框加滚动条显示信息

用tkinter进行界面程序开发中&#xff0c;经常需要将信息展示到界面上&#xff0c;给用户及时的反馈和想要看到的结果。Text控件允许用户以不同的样式、属性来显示和编辑文本&#xff0c;它可以包含纯文本或者格式化文本&#xff0c;同时支持嵌入图片、显示超链接以及带有 CSS …

【纳什博弈、ADMM】基于纳什博弈和交替方向乘子法的多微网主体能源共享研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Spring 注解之@RestController与@Controller的区别

目录 1&#xff1a;介绍 2&#xff1a;区别 3&#xff1a;总体来说 4&#xff1a;社区地址 1&#xff1a;介绍 RestController 和 Controller 是 Spring MVC 中常用的两个注解&#xff0c;它们都可以用于定义一个控制器类。 2&#xff1a;区别 返回值类型不同&#xff1a;…

使用插件快速生成代码

使用插件快速生成代码 咋们常说&#xff0c;授人以鱼不如授人以渔&#xff0c;在这里给大家提供一些技巧性的东西&#xff0c;方便一些新手同学可以快速上手&#xff0c;同时&#xff0c;也提高我们的开发兴趣与开发热情&#xff01; 主要讲什么呢&#xff0c;我们来学一学如何…

让AI来告诉你什么叫幽灵堵车

使用环境参考 CocosCreator v3.7.2 ChatGPT 正文 什么是幽灵堵车 堵车&#xff0c;大家都不陌生&#xff01; 堵车时我就思维发散&#xff0c;用 CocosCreator 模拟下堵车应该挺好玩&#xff0c;网上总说高速上最前面如果有个龟速的车&#xff0c;后面能堵车堵个两三公里。…

文本三剑客之sed

sed 一.概念 sed是一种流编辑器&#xff0c;流编辑器会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。 sed编辑器可以根据命令来处理数据流中的数据&#xff0c;这些命令要么从命令行中输入&#xff0c;要么存储一个命令文本文件中。二.工作流程 读取: sed从输入…

【论文阅读】MINOTAUR: Multi-task Video Grounding From Multimodal Queries

背景动机 细粒度的视频理解已经成为增强现实(AR)和机器人应用开发的关键能力。为了达到这种级别的视频理解&#xff0c;智能体(例如虚拟助手)必须具备识别和推理视频中捕获的事件和对象的能力&#xff0c;处理一系列视觉任务&#xff0c;如活动检测、对象检索和(空间)时间基础…

ChatGPT教程(终极版)

纯小白关于ChatGPT入门&#xff0c;你看我这篇文章就够了。 如果你已经用上了ChatGPT&#xff0c;更要恭喜你挖到宝藏&#xff0c;后面的高级技巧一定能让你有收获。 文章包含以下内容&#xff1a; 一、ChatGPT是啥&#xff1f;有什么用&#xff1b; 二、ChatGPT如何注册&…