C语言数据结构(4)——线性表其三(双向链表)

欢迎来到博主的专栏——C语言数据结构
博主ID:代码小豪

文章目录

    • 链表的种类
    • 头结点
    • 循环链表
    • 双向链表
    • 带头双向循环链表
      • 带头双向循环链表的定义与初始化
    • 空链表
      • 尾插法
      • 打印双向链表
      • 头插法
      • 查找指定数据项的节点
      • 在指定位置之后插入节点
      • 指定位置的删除
      • 双向链表的销毁
    • 顺序表与链表的对比

链表的种类

前面介绍了链表的种类之一——单链表,单链表的全称为不带头不循环单向链表

根据链表的性质,我们将链表分为以下几种:

(1)带头节点or不带头节点
(2)单向or双向
(3)循环与不循环

根据这些性质进行排列组合得出的链表共有八种

带头不带头
单向循环带头单向循环链表不带头单向循环链表
双向循环带头双向循环链表不带头双向循环链表
双向不循环带头双向不循环链表不带头双向不循环链表
单向不循环带头单向不循环链表不带头单向不循环链表

头结点

将有头结点的链表称为带头链表,没有头结点的链表称为不带头链表。

前面提到了指向第一个节点的指针被称为头指针,而头指针常常作为链表的函数返回值,如果出现了头指针需要不断修改的情况,可能会导致代码的繁冗。

比如写一个将多个链表进行合并的代码,头指针根据要求会有多种可能性,如果将这些可能性都进行判断的话,难免会让导致代码冗余。

在这里插入图片描述

那我们直接定义一个头结点,这个头结点的作用是当做一个链表的表头,但是不记录有效数据,只纪录指向合并后的链表的第一个节点。
在这里插入图片描述
可以发现,如果不带头结点,那么合并的链表需要区分头指针到底是list1或者其他,而带了头结点,只需要将listhead->next指向合并好的链表就行了。
即将

if(condition1)
return list1;
if(condition2)
return list2;
if(condition3)
return list3;
if(condition4)
return list4;

省略成

return listhead->next;

总结一下头指针的作用

头结点最主要的作用就是统一链表的操作。
(1)比如头指针为空的时候不能使用phead->next,但是头结点不会为空,所以可以使用listhead->next。
(2)有了头结点之后,删除和插入结点的时候不再需要判断头指针的指向问题,将任意位置的插入或删除节点的操作统一起来。(前面写的任意位置的插入节点的函数由于没有头节点,所以插入第一个节点前的位置需要用到头插法,进而导致代码冗余)

循环链表

将链表的最后一个结点的后继置为NULL的链表被称为不循环链表
在这里插入图片描述
如果我们要将链表实现多次遍历的操作时,不循环链表显然不满足要求,因为不循环链表遍历到表尾的时候就会停止遍历,无法进行多次遍历链表。

如果将表尾节点与表头节点连接起来,就能实现遍历表尾之后回到表头,重新遍历一次
在这里插入图片描述
循环链表的实现也非常简单,将表尾的next指向表头即可

ptail->next=phead;

双向链表

单链表的节点存储的只有一个指向后继元素的指针,这就会导致当节点来到下一个节点时,丢失上一个节点的地址,导致无法对当前节点的前驱节点进行操作。
在这里插入图片描述

为了解决这个问题,我们在定义链表的节点类型时可以加入一个指向前驱元素的指针,使得节点的前驱元素也被记录,这样子就能实现回退的操作。
在这里插入图片描述

带头双向循环链表

前篇文章讲了不带头单向不循环链表(单链表),已经了解了其中的三个特性,剩下的三个特性将通过讲解带头双向循环链表带着大家了解。

带头双向循环链表的定义与初始化

先来定义带头双向循环链表(后面简称双向链表)的节点数据类型。

双向链表的节点需要三个数据存储,分别是节点的数据,后继节点的指针,以及前驱节点的指针。

节点类型的定义如下:

typedef int LTData;
typedef struct ListNode
{
	LTData data;
	struct ListNode* next;
	struct ListNode* prev;
}Node;

由于双向链表具有头结点,因此需要对头结点进行创建与初始化。

在这里插入图片描述
前面提到了双向链表需要具备以下结构:
(1)带头
(2)循环
(3)双向
而头结点作为双向链表的一部分,在初始化的的时候也要满足以上要求,所以头结点应该初始化成这样:
在这里插入图片描述
头结点的初始化函数如下:

Node* HeadNodeInit(void)
{
	Node* head = (Node*)malloc(sizeof(Node));
	if (!head)
	{
		perror("Headinit fail");
		exit(EXIT_FAILURE);
	}

	head->data = -1;
	head->next = head;
	head->prev = head;
	return head;
}

使用这个函数将会生成一个头结点,并将该头节点返回。函数的调用方式如下:

Node* plist = HeadNodeInit();

空链表

不带头的链表如果为空,就将头指针置为NULL,或者将头指针为NULL的链表视为空链表。

而带头的链表如果为空链表,就说明链表当中只有一个头结点,将只有头结点的链表视为空链表。

比如上一章的单链表,如果要创造一个空链表,只需要声明一个头指针,并将头指针置为空即可

phead=NULL;

而若是创建带头的空链表,则需要创建并初始化头结点。

plist=HeadNodeInit();

尾插法

将头结点传入函数,通过头结点来对链表进行操作

void ListDataPushBack(Node*headnode,SLData n);

先来讲讲尾插的原理,假设当前链表中有多个节点
在这里插入图片描述
如果在表尾插入新的节点,首先要找到表尾,由上图可见,由于链表是循环的,只需要访问头结点的前驱节点即可找到尾结点。

ptail=headnode->prev;

接着便是将新的节点插入表尾。方法如下:

(1)将新的节点的前驱节点设为表尾
(2)将新节点的后继节点设为头结点
(3)将表尾的后继设为新节点
(4)将头结点的前驱设为新节点

完成后新的节点将会插入至链表的末尾部分
在这里插入图片描述
避免代码冗余,将生成新节点的代码封装成函数,后续生成新节点的操作,都会通过调用这个函数实现

Node* CreateNewNode(LTData n)
{
	Node* newnode = (Node*)malloc(sizeof(Node));
	if (newnode == NULL) {
		perror("newnode malloc fail");
		exit(EXIT_FAILURE);
	}

	newnode->data = n;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}

尾插法的函数实现如下:

void ListDataPushBack(Node* headnode,LTData n)
{
	Node* newnode = CreateNewNode(n);

	Node* ptail = headnode->prev;

	newnode->next = headnode;
	newnode->prev = ptail;
	ptail->next = newnode;
	headnode->prev = newnode;
}

打印双向链表

为了便于调式,可以封装一个打印双向链表的函数。

打印的方法如下:
通过遍历链表,将链表中除了头结点以外的所有节点的数据打印至屏幕。由于双向链表具有循环的性质,因此从第一个节点开始遍历至头结点为止。

void printlist(Node* headnode)
{
	Node* pcur = headnode->next;

	while (pcur!= headnode)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("headnode\n");
}

可以用打印的方式测试尾插法是否符合要求,测试方法如下:

	Node* plist = HeadNodeInit();
	ListDataPushBack(plist, 1);
	ListDataPushBack(plist, 2);
	ListDataPushBack(plist, 3);
	ListDataPushBack(plist, 4);

	printlist(plist);

打印结果如下,测试结果为尾插法符合要求。
在这里插入图片描述

头插法

将节点插入头结点之后,第一个节点之前的方法称为头插法。

头插法的方法如下:

(1)将新节点的后继节点设置为第一个节点
(2)将新节点的前驱节点设置为头结点
(3)将第一个节点的前驱节点设置为新节点
(4)将头结点的后继节点设置为新节点

注意步骤(3)和(4)的顺序不能调换。

在这里插入图片描述
代码如下:`

void ListDataPushFront(Node* headnode, LTData n)
{
	assert(headnode);

	Node* newnode = CreateNewNode(n);

	newnode->next = headnode->next;
	newnode->prev = headnode;

	headnode->next->prev = newnode;
	headnode->next = newnode;
}

查找指定数据项的节点

将整个链表遍历一遍寻找数据项,如果某节点的数据项符合查找的要求,就返回该节点位置。
函数原型如下:

Node* FindListNode(Node* headnode, LTData n)

(1)headnode是待查找的双向链表的头结点
(2)n是指定的数据项

遍历的方式如下:

(1)从头结点的下一个节点开始
(2)依次遍历所有节点,对比数据项
(3)回到头结点后结束遍历

Node* FindListNode(Node* headnode, LTData n)
{
	assert(headnode);//头结点不为空
	assert(headnode->next != headnode);//待查找的链表不为空链表

	Node* pcur = headnode->next;
	while (pcur != headnode)//循环结束条件为指针回到头结点
	{
		if (pcur->data == n)//匹配数据项
			return pcur;//完成匹配
		pcur = pcur->next;
	}
	return NULL;//未完成匹配
}

在指定位置之后插入节点

前面讲解了如何在查找指定的位置,这里就利用前面查找位置的函数来讲解如何在指定位置之后插入位置。

首先通过前面写的查找数据项的函数确定插入位置。
插入的方法如下:

(1)将新节点的前驱节点设为位置节点
(2)将新节点的后继节点设为位置节点的后一个节点
(3)令位置节点的后一个节点的前驱设为新节点
(4)令位置节点的后继节点设为新节点

注意步骤3和步骤4的位置不能变换
在这里插入图片描述
函数原型如下:

void ListNodeInsert(Node* headnode, LTData n, Node* pos);

(1)headnode——待插入链表的头结点
(2)n——节点的数据项
(3)pos——插入的节点位置

函数实现如下:

void ListNodeInsert(Node* headnode, LTData n, Node* pos)
{
	assert(pos);
	assert(headnode);

	Node*newnode=CreateNewNode(n);

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

指定位置的删除

删除指定位置的方法如下:

(1)将待删除的节点的后继节点的前驱设为待删除节点的前驱节点
(2)将待删除的节点的前驱节点的后继设为待删除节点的后继节点
(3)释放待删除节点
在这里插入图片描述

函数的原型如下:

void ListNodeDelete(Node* headnode, Node* pos);

(1)headnode是待删除节点的链表的头结点
(2)pos是待删除节点的位置

函数的实现如下:

void ListNodeDelete(Node* headnode, Node* pos)
{
	assert(headnode);
	assert(pos);
	assert(pos != headnode);//禁止删除头结点

	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;

	free(pos);
	pos = NULL;
}

双向链表的销毁

将双向链表销毁的方法如下:

从头结点开始往后遍历(也可以往前遍历),将每个遍历的节点进行销毁,最后回到头结点时结束遍历,别忘了头结点也要进行销毁。

函数的代码如下:

void ListDestory(Node* headnode)
{
	Node* pcur = headnode->next;
	Node* ptmp = pcur;
	
	while (pcur != headnode)
	{
		ptmp = pcur;
		pcur = pcur->next;
		free(ptmp);
	}

	free(headnode);
}

顺序表与链表的对比

既然顺序表与链表都是线性表,那么这两种线性表的优劣是什么呢

顺序表线性表
物理结构顺序存储非线性存储
访问元素时间复杂度O(1)时间复杂度O(n)
插入/删除元素O(N)O(1)
空间利用需要扩容(利用程度低)不需要扩容(利用程度高)
优势查找元素和访问速度快频繁插入删除的速度快

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

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

相关文章

东北老铁带你总结《Java IO 模型》

东北老铁带你总结《Java IO 模型》 文章目录 东北老铁带你总结《Java IO 模型》前言I/O何为 I/O?有哪些常见的 IO 模型? Java 中 3 种常见 IO 模型BIO (Blocking I/O)NIO (Non-blocking/New I/O)AIO (Asynchronous I/O) IO 模型这块确实挺难理解的,需要太多计算机…

漏洞原理linux操作系统的SqlMap工具的使用

漏洞原理linux操作系统的SqlMap工具的使用 Linux操作系统基础操作链接: 1024一篇通俗易懂的liunx命令操作总结(第十课)-CSDN博客 kali的IP地址:192.168.56.1 实操 # kali中使用sqlmap http://192.168.56.1/ sqlmap -u http://192.168.56.1/news/show.php?id46 sqlmap -u …

IT界含金量高的证书,除了软考证书,还有这15种

文章目录 计算机技术与软件专业技术资格考试全国计算机信息高新技术考试思科认证微软认证:华为认证IBM认证国家信息安全水平考试注册信息安全专业人员注册信息安全渗透测试工程师项目管理专业人士资格认证Red Hat认证CompTIA 认证CISSP认证Oracle认证Sun认证AWS认证…

sqli.labs靶场(8-17关)

8、第八关(布尔盲注) id1显示You are in...........,id1单引号不显示,id1 --显示正常 这个应该是单引号闭合,接下来就和第七关差不多上脚本 爆库名长度:id1%27%20and%20length(database()){i}%20-- 爆库…

xcode安装visionOS Simulator模拟器报错解决方法手动安装方法

手动安装方法: 手动下载visionOS Simulator模拟器地址: https://developer.apple.com/download/all/ 选择 Xcode 版本 sudo xcode-select -s /Applications/Xcode.app # 用 Xcode-beta 的话是: # xcode-select -s /Applications/Xcode-beta…

如何将DDD应用到基础设施设计?

前段时间在面试的时候,面试官问到:你是如何将DDD(领域驱动设计)应用到基础设施的? 我很惊讶,终于有人问我这个问题了。 在过去从事基础设施(DevOps、SRE、运维)的5年里,我…

爬虫基础-前端基础

Html是骨骼、css是皮肤、js是肌肉,三者之间的关系可以简单理解为m(html)-v(css)-c(js) 浏览器的加载过程 构建dom树 子资源加载-加载外部的css、图片、js等外部资源 样式渲染-css执行 DOM树 ajax、json、xml AJAX 是一种在无需重新加载整个网页的情况下&#xf…

基于springboot校园台球厅人员与设备管理系统源码和论文

在Internet高速发展的今天,我们生活的各个领域都涉及到计算机的应用,其中包括校园台球厅人员与设备管理系统的网络应用,在外国管理系统已经是很普遍的方式,不过国内的管理网站可能还处于起步阶段。校园台球厅人员与设备管理系统具…

Unity中使用Ultraleap的InteractionButton组件

本节在上一节基础上进行,上一小结参考如下: Unity中创建Ultraleap 3Di交互项目 本节工程文件如下: Unity中使用Ultraleap的InteractionButton组件 本节结构有所更改,主要是参考官方示例结构进行重新调整,和上一小节相…

STM32与FPGA实现以太网功能--ping

方案: ①stm32与88E6320的一个RMII接口连接,实现网管功能。 ②FPGA与88E6320的另一个RMII接口连接,使用UDP实现业务数据传输。 ③stm32与FPGA中MAC地址不同,但是IP使用相同 结果: 1、在局域网点对点通信正常。 2…

【C语言/数据结构】排序(快速排序及多种优化|递归及非递归版本)

🌈个人主页:秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343🔥 系列专栏:《数据结构》https://blog.csdn.net/qinjh_/category_12536791.html?spm1001.2014.3001.5482 ​​​​ 目录 交换排序 快速排序 hoare版代…

前端qrcode生成二维码详解

文章目录 前言1、浏览器支持2、优点3、缺点4、相关方法5、安装及使用示例 前言 qrcode 是一个基于JavaScript的二维码生成库,主要是通过获取 DOM 的标签,再通过 HTML5 Canvas 绘制而成,不依赖任何库。 官方文档:https://www.npm…

NetExec:一款功能强大的自动化网络安全评估与漏洞测试工具

关于NetExec NetExec是一款功能强大的自动化网络安全评估与漏洞测试工具,该工具可以帮助广大研究人员以自动化的形式测试大型网络的安全,并通过利用网络服务漏洞来评估目标网络的安全态势。 支持的协议 1、SMB协议 2、LDAP协议 3、WinRM协议 4、MSSQL协…

二叉树--199. 二叉树的右视图/medium 理解度C

199. 二叉树的右视图 1、题目2、题目分析3、复杂度最优解代码示例4、适用场景 1、题目 给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 示例 1: 输入: [1,2,3,null,5,null,4] 输出…

IT问题解答类型网站源码,多用户博客文章程序

问答网是一款为IT工程师提供的问答平台,旨在帮助用户在线获取专业知识和相关问题的答案。在问答网,用户可以轻松找到其他人的问答问题,并在这里寻求解答。如果您有任何想要解决的问题,都可以在此发布问题并得到其他同行的解答。 该平台提供多项功能,支持支付宝在线付款,…

数字艺术展厅有什么好处,搭建数字艺术展厅要注意什么

引言: 数字艺术展厅是一种利用数字科技手段搭建的艺术展览空间,通过数字化展示艺术品,能够为观众带来全新的艺术体验。那么数字艺术展厅有什么好处,搭建数字艺术展厅要注意什么呢? 一、数字艺术展厅的好处 1.创新艺术…

STM32连接阿里云物联网平台

文章目录 引言一、STM32连接阿里云物联网平台思路二、ESP8266烧录固件三、使用AT指令连接阿里云物联网平台四、STM32环形串口缓冲区驱动程序五、STM32连接阿里云驱动程序 引言 连续写了两篇关于阿里云连接的文章,都是使用Arduino ESP8266 & Arduino ESP32的方式…

MG7050VAN 基于声表的差分多输出 晶体振荡器(LVDS)

MG7050VAN的LVDS输出是一款差分多输出晶体振荡器,具有极低的抖动和超强的稳定性。该振荡器适用于多种应用场景,如服务器、存储器和网络仪器等。作为一款高端的LVDS输出设备,该振荡器在输出性能上具有明显优势。其超低抖动水平可以达到0.3ps M…

子窗口的qss应该放在,它构造函数里

子窗口 构造函数 加载qss QFile qss(QString(":/simulator/resources/main_theme.qss")); if (qss.open(QIODevice::ReadOnly)) {auto content qss.readAll();this->setStyleSheet(content);qss.close(); }黑色样式 放在main_theme.qss #init_scene{ backgro…

NPDP证书:让你的职业生涯飞升!

🌟没错!NPDP证书正在成为产品经理们的“新宠”!越来越多的同行们纷纷选择考取NPDP证书,为什么这么火爆?一起来探究下吧! 🚀NPDP认证:产品经理的国际通行证 📍NPDP&#x…