DS:带头双向循环链表的实现(超详细!!)

创作不易,友友们给个三连吧!!!

      博主的上篇文章介绍了链表,以及单链表的实现。

单链表的实现(超详细!!)
    其实单链表的全称叫做不带头单向不循环链表,本文会重点介绍链表的分类以及双链表的实现!

一、链表的分类

   链表的结构⾮常多样,组合起来就有8种(2 x 2 x 2)链表结构:

1.1 单向或者双向

    双向链表,即上一个结点保存着下一个结点的地址,且下一个结点保存着上一个结点的地址,即我们可以从头结点开始遍历,也可以从尾结点开始遍历

1.2 带头或者不带头 

     单链表中我们提到的“头结点”的“头”和“带头”链表的头是两个概念!单链表中提到的“头结点”指的是第一个有效的结点,“带头”链表里的“头”指的是无效的结点(即不保存任何有效的数据!)

    

1.3 循环或者不循环

     不循环的链表最后一个结点的next指针指向NULL,而循环的链表,最后一个结点的next指针指向第一个结点!!

      虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 单链表(不带头单向不循环链表)和 双向链表(带头双向循环链表)

1. 无头单向非循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。

2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

二、带头双向循环链表的结构

      带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨的”

“哨兵位”存在的意义:遍历循环链表避免死循环。

三、双向链表结点结构体的创建

     与单链表结点结构体不同的是,双向链表的结点结构体多了一个前驱结点!!

typedef int LTDataType;//对类型进行重命名,后面可以通过修改存储其他数据类型
typedef struct ListNode
{
	LTDataType data;//保存的数据
	struct ListNode* prev;//指针保存前一个结点的地址
	struct ListNode* next;//指针保存后一个结点的地址
}LTNode;

四、带头双向循环链表的实现

4.1 新结点的申请

      涉及到需要插入数据,都需要申请新节点,所以优先封装一个申请新结点的函数!利用返回值返回该结点

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);//申请失败需要强制退出程序
	}
	//申请成功,则新节点的前驱结点和后驱结点都指向自己
	newnode->data = x;
	newnode->prev = newnode->next = newnode;
    return newnode;
}

4.2 初始化(哨兵位结点)

       对于双向链表来说,需要优先创建一个哨兵结点,和其他结点不同的是,该哨兵结点可以不存储数据,这里我们默认给他一个-1。并利用返回值返回该结点。

LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1
	return phead;//返回哨兵结点
}

4.3 尾插 

       如图,因为这个一个循环链表,相当于我们要把新节点插在最后一个结点和哨兵结点之间,并且最后一一个结点可以用哨兵结点的前驱结点(phead->prev)就可以找到,然后建立phead phead->prev newnode的联系!

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立phead phead->prev newnode的联系
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev->next = newnode;//尾结点的后继指针指向新节点
	phead->prev = newnode;//哨兵结点的前驱指针指向新结点
}

 单链表中我们的参数选择二级指针,为什么这里选择一级指针???

      对于单链表来收,单链表的头节点是会改变的,所以我们需要用二级指针,但是双链表的头节点相当于哨兵位,哨兵位是不需要被改变的,他是固定死的,所以我们选择了一级指针。(单链表改了完全头节点,但是双链表只会改变头结点的成员——prev和next)

注:phead->prev->next = newnode和phead->prev = newnode不能替换顺序,因为尾结点是通过头节点找到的,所以要优先让他与newnode建立联系,双链表虽然不需要像单链表一样找最后一个结点需要遍历链表,但是要十分注意修改指针指向的先后顺序!!

4.4 头插

       如图可知,相当于将新节点插入在头节点和头节点下一个结点之间,头节点下一个结点可以通过phead->next找到,然后建立phead、phead->next、newnode的联系!!

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立phead phead->next newnode的联系
	newnode->prev = phead;
	newnode->next = phead->next;
	phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点
	phead->next = newnode;//头节点的后继指针指向新节点
}

4.5 打印

      因为是循环链表,所以为了避免死循环打印,我们要设置一个指针接收头节点的下一个结点,然后往后遍历,直到遍历到头节点结束。

void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

4.6 尾删

      由图可知,要建立phead和phead->prev->prev的联系,同时由于还要释放最后一个结点(phead->prev),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!同时要注意一条规则,就是当链表中只有哨兵结点的时候,我们称该链表为空链表!因此如果链表只存在哨兵结点,那么删除是没有意义的,所以必须断言!

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//链表只有哨兵结点时删除没意义
	LTNode* del = phead->prev;//del记录最后一个结点
	del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点
	phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点
	free(del);//释放最后一个结点
	del = NULL;
}

4.7 头删

       由图可知,要建立phead和phead->next->next的联系,同时由于还要释放第二个结点(phead->next),所以在建立联系之前要现保存这个被释放的空间,等建立联系完再释放!!

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//链表只有哨兵结点时删除没意义
	LTNode* del = phead->next;//del记录第二个结点
	del->next->prev = phead;//第二个结点的前驱指针指向头结点
	phead->next = del->next;//头节点的后驱指针指向第三个结点
	free(del);//释放第二个结点
	del = NULL;
}

4.8 查找

     涉及到对指定位置进行操作的时候,需要设置一个查找函数,根据我们需要的数据返回他的结点地址

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)//遍历链表
	{
		if (pcur->data == x)
			return pcur;//找到的话返回该结点
		pcur = pcur->next;
	}
	//循环结束还是没找到
	return NULL;
}

4.9 指定位置之后插入

        由图可知,指定位置插入相当于将新结点插入到指定位置(pos)和指定位置下一个结点的位置(pos->next),然后建立pos pos->next newnode的联系,而且这里用不到头节点!

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//保证pos为有效结点
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立pos pos->next newnode的联系
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点
	pos->next = newnode;//pos结点的后继结点指向新结点
}

4.10 指定位置删除

       右图可知建立指定位置的前一个结点(pos->prev)和指定位置的后一个结点(pos->next)的联系,并释放pos。

void LTErase(LTNode* pos)
{
	assert(pos);//保证pos为有效结点
	pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点
	pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点
	free(pos);//释放pos
	pos = NULL;
}

4.11 销毁链表

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	LTNode*next = NULL;
	while (pcur != phead)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//除了头结点都释放完毕
	free(phead);
	//phead = NULL;//没有用!
}

为什么phead=NULL没有用??

       因为我们使用的是一级指针,这里相当于是值传递,值传递形参改变不了实参,所以将phead置空是没有意义的,其实如果这里使用二级指针,然后传地址就可以了,但是为了保持接口一致性,我们还是依照这种方法,但是phead=NULL必须在主函数中去使用,所以我们在调用销毁链表的函数的时候,别忘记了phead=NULL!!

五、带头双向循环链表实现的全部代码

List.h

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

typedef int LTDataType;//对类型进行重命名,后面可以通过修改存储其他数据类型
typedef struct ListNode
{
	LTDataType data;//保存的数据
	struct ListNode* prev;//指针保存前一个结点的地址
	struct ListNode* next;//指针保存后一个结点的地址
}LTNode;


LTNode* LTBuyNode(LTDataType x);//申请新的链表结点
LTNode* LTInit();//初始化(申请一个哨兵结点)
void LTPushBack(LTNode* phead, LTDataType x);//尾插 (最后一个结点后插入或哨兵结点前插入)
void LTPushFront(LTNode* phead, LTDataType x);//头插 (哨兵结点后的插入)
void LTPrint(LTNode* phead);//打印
void LTPopBack(LTNode* phead);//尾删
void LTPopFront(LTNode* phead);//头删
LTNode* LTFind(LTNode* phead, LTDataType x);//查找
void LTInsert(LTNode* pos, LTDataType x);//指定位置之后插入
void LTErase(LTNode* pos);//指定位置删除
void LTDestroy(LTNode* phead);//销毁链表

List.c

#include"List.h"

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(1);//申请失败需要强制退出程序
	}
	//申请成功,则新节点的前驱结点和后驱结点都指向自己
	newnode->data = x;
	newnode->prev = newnode->next = newnode;
	return newnode;
}

LTNode* LTInit()
{
	LTNode* phead = LTBuyNode(-1);//哨兵结点可以不存储数据,我们默认给个-1
	return phead;//返回哨兵结点
}

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立phead phead->prev newnode的联系
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev->next = newnode;//尾结点的后继结点指向新节点
	phead->prev = newnode;//哨兵结点的前驱指针指向新结点
}

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立phead phead->next newnode的联系
	newnode->prev = phead;
	newnode->next = phead->next;
	phead->next->prev = newnode;//头节点的下一个结点的前驱指针指向新结点
	phead->next = newnode;//头节点的后继指针指向新节点
}

void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//链表只有哨兵结点时删除没意义
	LTNode* del = phead->prev;//del记录最后一个结点
	del->prev->next = phead;//倒数第二个结点的后驱指针指向头结点
	phead->prev = del->prev;//头节点的前驱结点指向倒数第二个结点
	free(del);//释放最后一个结点
	del = NULL;
}

void LTPopFront(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//链表只有哨兵结点时删除没意义
	LTNode* del = phead->next;//del记录第二个结点
	del->next->prev = phead;//第二个结点的前驱指针指向头结点
	phead->next = del->next;//头节点的后驱指针指向第三个结点
	free(del);//释放第二个结点
	del = NULL;
}

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)//遍历链表
	{
		if (pcur->data == x)
			return pcur;//找到的话返回该结点
		pcur = pcur->next;
	}
	//循环结束还是没找到
	return NULL;
}

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//保证pos为有效结点
	LTNode* newnode = LTBuyNode(x);//申请新节点
	//建立pos pos->next newnode的联系
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next->prev = newnode;//pos的后一个结点的前驱结点指向新节点
	pos->next = newnode;//pos结点的后继结点指向新结点
}

void LTErase(LTNode* pos)
{
	assert(pos);//保证pos为有效结点
	pos->prev->next = pos->next;//pos的前一个结点的后继指针指向pos后一个结点
	pos->next->prev = pos->prev;//pos的后一个结点的前驱指针指向pos的前一个结点
	free(pos);//释放pos
	pos = NULL;
}

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	LTNode*next = NULL;
	while (pcur != phead)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//除了头结点都释放完毕
	free(phead);
	//phead = NULL;//没有用!
}

六、顺序表和链表的优缺点分析

1、存储空间

顺序表物理上连续

链表逻辑上连续,但是物理上不连续

2、随机访问

顺序表可以通过下标去访问

链表不可以直接通过下标去访问

3、任意位置插入或者删除元素

顺序表需要挪移元素,效率低

链表只需修改指针指向

4、插入

动态顺序表空间不够时需要扩容

链表没有容量的概念

5、应用场景

顺序表应用于元素高效存储+频繁访问的场景

链表应用于任意位置插入和删除频繁的场景

总之:没有绝对的优劣,都要各自适合的应用场景!!

 

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

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

相关文章

STP生成树协议实验

实验大纲 一、什么是生成树协议 二、生成树原理 1.STP工作原理 2.STP主要参数 3.STP根网桥 4.STP协议版本 三、实验 1.构建网络拓扑结构图 2.配置IP地址&#xff08;8台PC机&#xff09;&#xff1a;192.168.7.1~192.168.7.8 3.配置SW1 4.配置SW2 5.配置SW3 6.配置…

浪潮信息打造高效算力架构 为金融业数字化坚实基座

新时期&#xff0c;数据智能已经逐渐成为金融商业中的重要力量&#xff0c;构建更强大的算力系统&#xff0c;推动金融业务的高效发展&#xff0c;已经成为了金融行业的目标。对此&#xff0c;浪潮信息也为金融客户提供了崭新的解决方案。此前&#xff0c;某银行基于浪潮信息量…

第二模块 函数模块

第二模块 函数&模块 day09 文件操作相关1. 文件操作1.1 读文件1.2 写文件1.3 文件打开模式1.4 常见功能1.5 上下文管理练习题 2.csv格式文件3.ini格式文件4.XML格式文件4.1 读取文件和内容4.2 读取节点数据4.3 修改和删除节点4.4 构建文档 5.Excel格式文件5.1 读Excel5.1 写…

ESP32 SPIFFS文件系统

简介 本章涉及知识点&#xff1a;ESP32 SPIFFS文件系统、日志输出。 ESP-IDF版本&#xff1a;V5.1.2 源码 小智学长的源码&#xff1a;DesktopScreen 7 文件系统 系统配置 如果是自己构建的项目&#xff0c;如图。要在CMakeLists中配置上spiffs。 如果是直接跑官方例程则忽略系…

wireshark利用sshdump自身组件进行远程实时抓包过滤

引言 以前在不了解wireshark可以远程抓包的时间&#xff0c;经常通过tcpdump在远程linux主机将抓包文件保存下来后&#xff0c;然后拖拽入windows中再打开&#xff0c;进行分析查看。 此过程比较繁琐&#xff0c;也不够实时。比较常用的抓包动作是仅出现某特征的报文后&#…

手动导入jar包到Maven的解决方案(简单有效!)

想要导入一个jar包到项目中&#xff0c;这个jar包在Maven中没有可以尝试以下方式。 第一步 先找到你maven的本地仓库&#xff0c;我的仓库就在这里&#xff0c;你可以根据你安装的maven找到你的目录 第二步 根据坐标创建文件夹。 这个依赖modbus4j.jar&#xff0c;Maven远…

Cesium.js实现显示点位对应的自定义信息弹窗(数据面板)

博客&#xff1a;关于Cesium的常见需求整理之点位和弹窗&#xff08;点位弹窗&#xff09; 博客&#xff1a;cesium添加点、线、面、文字、图标、模型等标绘 零、相关技术选型&#xff1a; Vue2 Vuecli5 Cesium.js 天地图 一、需求说明 在使用2D地图&#xff08;天地图、高德…

微信小程序(二十一)css变量-定义页面主题色

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.使用css变量 2.消除按钮白块影响 3.修改图标样式 源码&#xff1a; npmTest.json {"navigationStyle": "custom","usingComponents": {//引入vant组件"van-nav-bar"…

低代码助力软件开发

随着企业对于低代码开发平台的需求日益增长&#xff0c;急需一个通用的解决方案来满足各种低代码平台的开发需求。正是在这种情况下&#xff0c;低代码引擎应运而生。 作为一种通用的开发框架&#xff0c;通过对低代码平台系统常用的功能进行解构&#xff0c;将其划分为多个功能…

2. HarmonyOS 应用开发 DevEco Studio 准备-2

2. HarmonyOS 应用开发 DevEco Studio 准备-2 首选项设置 中文设置 主题 字体 插件安装和使用 保存时操作 编辑器 工程树管理 代码树管理 标记 字符串可视化编辑 参考文档 常用快捷键 编辑 查找或替换 编译与运行 调试 其他 预览 页面预览 自定义组件预览 预览…

行测-资料:3. 比重、平均数

1、比重 1.1 现期比重★★★ C A&#xff0c;16.63%≈1/6 B C&#xff0c;拆成 50% 和 6.6% ≈ 1/15。 C D 1.2 基期比重★ 数学推导&#xff0c;A&#xff0c;B&#xff0c;A/(1 a)&#xff0c;B / (1 b) A&#xff0c;4 / 9&#xff0c;12 / 27 x 1.14 / 1.18&#xff0c;看…

基于Python flask MySQL 猫眼电影可视化系统设计与实现

1 绪论 1.1 设计背景及目的 猫眼电影作为国内知名的电影信息网站&#xff0c;拥有海量的电影信息、票房数据和用户评价数据。这些数据对于电影市场的研究和分析具有重要意义。然而&#xff0c;由于数据的复杂性和数据来源的多样性&#xff0c;如何有效地采集、存储和展示这些数…

c语言基础6

1.逗号表达式 逗号表达式&#xff0c;就是用逗号隔开的多个表达式。 逗号表达式&#xff0c;从左向右依次执行。整个表达式的结果是最后⼀个表达式的结果。 我们来看下面的一个代码&#xff1a; int main() {int a 1;int b 2;int ret (a > b, a b 2, b, b a 1);p…

程序员成被裁最多的职业,互联网成围城,“转码”神话破灭?

随着互联网蓬勃发展&#xff0c;“转码”一直被视为找不到工作时的灵丹妙药。所谓转码&#xff0c;就是转行成为程序员。专业太偏&#xff1f;没关系&#xff0c;可以转码。失业了&#xff1f;没关系&#xff0c;可以转码。不知道该做什么工作&#xff1f;那就转码吧。程序员薪…

资产盘点系统架构与实践

资产盘点系统架构与实战 随着企业规模的不断扩大&#xff0c;资产管理变得越来越重要。为了更好地管理企业资产&#xff0c;许多公司都开始使用资产盘点系统。本文将介绍资产盘点系统的架构和实战。 一、概述 资产盘点系统是一种用于管理企业资产的软件系统。它可以帮助企业…

抽象工厂模式-C#实现

该实例基于WPF实现&#xff0c;直接上代码&#xff0c;下面为三层架构的代码。 一 Model using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace 设计模式练习.Model.抽象工厂模式 {public abstrac…

MPNN(Message Passing Neural Network)、graph pooling 、unpooling

The state encoder is mainly composed of MPNN layers organized into DenseNet blocks, which use graph pooling and unpooling layers (see Section S1.5†) to reduce the memory cost during training.

华为机考入门python3--(0)模拟题3-计算字符串重新排列数

分类&#xff1a;排列组合 知识点&#xff1a; 计算字符串中每个字符出现的次数 Counter(string) 计算列表中每个元素出现的次数 Counter(list) 阶乘 math.factorial(num) 排列去重 题目来自【华为招聘模拟考试】 先把每个字符当成唯一出现过一次&#xff0c;计算所有排列…

CSS 之 图片九宫格变幻效果

一、简介 ​ 本篇博客用于讲解如何实现图片九宫格变幻的样式效果&#xff0c;将图片分为九块填充在33的的九宫格子元素中&#xff0c;并结合grid、hover、transition等CSS属性&#xff0c;实现元素hover时&#xff0c;九宫格子元素合并为一张完整图片的动画效果。 ​ 为了简化…

【Linux C | 进程】Linux 进程间通信的10种方式(1)

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…