数据结构·单链表

        不可否认的是,前几节我们讲解的顺序表存在一下几点问题:

        1. 中间、头部的插入和删除,需要移动一整串数据,时间复杂度O(N)

        2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗

        3. 增容一般是2倍的增长,这势必会造成空间的浪费

        那如何解决这些问题呢,此时,链表出现了

1. 链表的概念和结构

        我们之前说过,线性表的特点就是逻辑上是连续的,物理上不一定连续。顺序表是逻辑上是连续的,物理上也是连续的。而今天的链表就是逻辑上是连续的,但是物理上是不连续的

        最简单的链表是由节点们串在一起组成的,每个节点包含了两个内容:

                1. 要存入的有效数据

                2. 下一个节点的地址

        可以看出,每个节点在物理上都是独立的,不连续的。但是每个节点在逻辑上又有关联,每个节点都知道下一个节点的指针,要找到下一个节点就访问那个指针就好了

        具体来讲就是把 plist 当成一个钥匙,最开始保存的是第一个节点的地址,用完第一个节点的数据之后,把第一个节点中存储的地址再给到plist,这样plist就可以开第二个节点了,以此类推···

        下面我们依照上面的图片做一个简单的节点结构

                                

        到这里,链表的地基就学会了,下面我们尝试实现一下

2. 单链表(Single linked list)的实现

        跟顺序表一样,先是把准备工作,三个文件准备好

                                

        再把链表节点写出来

                                 ​​​​​​

        在这里我要重点声明一下,接下来如果从主函数前去访问链表时用的都是plist参数,从主函数中给子函数传参也是plist,就像这样

        ​​​​​​​        ​​​​​​​                

        然后就正式进入链表实现啦,大家伙坐稳喽

2.1 链表的打印

        打印的逻辑就是先拿到第一个节点的地址 pcur,把这个地址访问到 data 打印出来,然后将pcur的内容变成下一个要打印的节点的地址,直到pcur的内容是NULL为止

        ​​​​​​​        ​​​​​​​        

2.2 链表的插入

        因为插入就一定需要申请一个新的节点,所以我们先把这个功能封装好

        向堆区申请一块空间用来存放节点,记录这个节点的地址

        当然,如果你想把newnode的类型改成 SLTNode 也可以,不过后面要用到节点地址的时候就要取地址一下,很麻烦,所以我们干脆直接返回节点的地址

2.2.1 尾插

        在链表的尾端插入一个数据。

        因为如果链表为空(没有节点)的时候要修改 plist 的内容,让它指向我们新添加的第一个节点,所以我们传参的时候要传 &plist ,因此函数参数要用二级指针来接收这个可能会被修改的plist

        如果链表不为空,就去找尾节点,把为节点的next成员内容从NULL变成我们新添加的节点地址,可以这么理解:

        这个图里有一点不恰当,就是这个 pphead 要解引用一次 (*pphead) 才能找到第一个节点的地址

        ​​​​​​​

        接下来我们运行一下看看效果

2.2.2 头插

        头插比尾插好理解一点,直接上思路图(画的太丑了QAQ)

        

        很明显,链表是否为空对于需要的操作是没有影响的,上代码:

        

        最后运行一下看结果:

        

        因为每次都是把节点插到最前面,所以反着打出来是对的

2.3 链表的删除

2.3.1 尾删

        尾删的逻辑就是找到最后一个节点 ptail 和倒数第二个节点 prev ,把倒数第二个节点的next成员置为空指针,释放掉最后一个节点。当然,如果链表为空,也就是说没有节点的话就不能执行删除操作,用assert断言报错

        ​​​​​​​        ​​​​​​​        

        上代码:

        

2.3.2 头删

        头删也是需要两个指针控制,要注意的就是要先释放掉*pphead也就是第一个节点,然后再把*pphead的内容改成第二个节点的地址,接上第二个节点

        ​​​​​​​              ​​​​​​​

        代码如下:

        ​​​​​​​        

2.4 查找

        链表的查找很简单,就是遍历链表,找到了就返回节点地址,没找到就返回空指针

        

2.5 在任意位置插入数据

2.5.1 在指定位置前插入数据

        可以用SLTFind找到要被前插的节点的地址pos,在这个节点前面插入节点,还需要直到它前面那个节点的地址prev

        ​​​​​​​        ​​​​​​​        

        在实现这个功能的时候我们要注意,当pos是头节点的情况:

        

        下面使用一下

        

2.5.2 在指定位置后插入数据

        这个比较简单,但是要注意给地址的顺序,要先把后面那个节点的地址给到新节点,再把指定位置pos节点的地址成员改成新节点的地址,否则就会导致后面那个节点地址的丢失,没办法接到新节点后面了

        还有就是我们不需要知道链表的头节点是什么了,只需要关注pos就行了

        ​​​​​​​                ​​​​​​​

        

2.6 在任意位置删除节点

2.6.1 删除pos节点

        删除pos节点要先知道它前面的那个节点prev,然后把prev跟pos后面那个节点先连起来,最后再把pos释放掉。还有要注意的一点就是当pos就是链表头节点的时候要特殊处理一下

        ​​​​​​​        ​​​​​​​          

        

2.6.2 删除pos后面的一个节点

        这个功能也是只需要关注pos后面的内容就行,所以只需要传pos一个参数。还要注意一点就是pos不能是链表中的最后一个节点,否则它后面没有节点了还删什么

        ​​​​​​​        

        ​​​​​​​        

2.7 链表的销毁

        两个变量,pcur记录当前要准备销毁的节点地址,next记录下一个节点地址,防止销毁上一个节点之后找不到下一个节点了。然后两个变量一直循环向后扫描销毁,直到pcur指向NULL

        ​​​​​​​                ​​​​​​​

                        

3. 链表的分类

        链表按带头或不带头,单向或双向,循环或不循环,排列组合有8种

        我们刚刚学的单链表全称就是:不带头单向不循环链表

        带头不带头是说链表有没有一个不存储有效数据的节点,放在第一个存放有效数据节点之前

        ​​​​​​​        ​​​​​​​        

        单向双向是说链表能通过后一项找到前一项就是双向的,如果只能根据前一项找到后一项链表就是单项的。或者说双向链表的节点中的两个存放地址的成员中,一个存下一个节点的地址,一个存上一个节点的地址。

        ​​​​​​​        ​​​​​​​        

        循环不循环是说最后一个节点指向第一个节点就是循环链表,要是最后一个节点指向NULL就是不循环链表

                        

        虽然链表的种类很多,但是常用的只有两种:

                1.单链表(不带头单向不循环链表)

        单链表结构简单,一般不会单独用来存贮数据,它一般作为其他数据结构的子结构出现

                2.双向链表(带头双向循环链表)

        双向链表结构最复杂,一般用来单独存储数据。它虽然复杂,但是之后实现它的实际就会发现它有很多优势,致使实现它反而变得简单了,后面会有实现它的章节的。

4. 本节代码

        SList.h

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

//链表是由节点构成的
typedef int SLTDataType;

typedef struct SListNode 
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;


//链表的打印
void SLTPrint(SLTNode* phead); 

//链表的插入
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);

//链表的头删和尾删
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);


//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x);


//在任意位置插入数据
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//在任意位置删除节点
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos后面的一个节点
void SLTEraseAfter(SLTNode* pos);

//销毁链表
void SLTDestory(SLTNode** pphead);

        SList.c

#include"SList.h"

//链表的打印	
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}


//申请一个新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}


//链表的插入
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//申请一个新节点
	SLTNode* newnode = SLTBuyNode(x);

	//链表为空,新节点作为头
	if (*pphead == NULL)
	{
		*pphead = newnode;
		return;
	}

	//链表不为空,找尾节点
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	//找到尾节点了
	ptail->next = newnode;
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//申请一个新节点
	SLTNode* newnode = SLTBuyNode(x);

	//链表为不为空,操作都一样
	//斩断第一个连接,再把新节点接进去
	newnode->next = *pphead;
	*pphead = newnode;
}




//链表的头删和尾删
//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	//链表不能为空
	assert(*pphead);

	//链表不为空
	//链表只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//链表有多个节点
	SLTNode* ptail = *pphead;
	SLTNode* prev = NULL;
	//找到尾节点
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = NULL;
	free(ptail);
	ptail = NULL;
	
}

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	//链表不能为空
	assert(*pphead);

	//让第二个节点变成新的头节点
	//释放旧的头节点
	SLTNode* sec = (*pphead)->next;
	free(*pphead);
	*pphead = sec;
}



//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//遍历链表
	SLTNode* pcur = *pphead;

	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}

	return NULL;
}




//在任意位置插入数据
//在指定位置前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	//这里断言*pphead不能为空
	//因为指定节点的地址pos不能为空,所以这个链表也不能为空
	assert(*pphead);

	//当pos是头节点,执行头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
		return;
	}

	//当pos不是头节点
	//申请一个新节点
	SLTNode* newnode = SLTBuyNode(x);

	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = newnode;
	newnode->next = pos;
}

//在指定位置后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);

	//申请一个新节点
	SLTNode* newnode = SLTBuyNode(x);

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




//在任意位置删除节点
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);

	//如果pos指向头节点,执行头删
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
		return;
	}

	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

//删除pos后面的一个节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//pos->next不能为空
	//就是说pos不能是最后一个节点
	assert(pos->next);

	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}



//销毁链表
void SLTDestory(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	SLTNode* pcur = *pphead;
	SLTNode* next = NULL;
	while (pcur)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

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

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

相关文章

MySQL安装及可视化工具SQLyog下载

编程如画&#xff0c;我是panda&#xff01; 最近学习Web开发的时候要用到数据库&#xff0c;一开始下载的ZIP版本的&#xff0c;还得修改配置文件&#xff0c;挺麻烦的&#xff0c;后来发现可以直接使用msi版的安装包疯狂next&#xff0c;所以就出一期教程。 前言 MySQL 是一…

链表--104. 二叉树的最大深度/medium 理解度A

104. 二叉树的最大深度 1、题目2、题目分析3、复杂度最优解代码示例4、适用场景 1、题目 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,n…

22款奔驰GLS450升级香氛负离子车载香薰功能

奔驰原厂香氛系统激活原车自带系统&#xff0c;将香气加藏储物盒中&#xff0c;通过系统调节与出风口相结合&#xff0c;再将香味传达至整个车厢&#xff0c;达到净化车厢空气的效果&#xff0c;让整个车厢更加绿色健康&#xff0c;清新淡雅。星骏汇小许Xjh15863 产品功能&…

Vue3+Vite使用Puppeteer进行SEO优化(SSR+Meta)

1. 背景 【笑小枫】https://www.xiaoxiaofeng.com上线啦 资源持续整合中&#xff0c;程序员必备网站&#xff0c;快点前往围观吧~ 我的个人博客【笑小枫】又一次版本大升级&#xff0c;虽然知道没有多少访问量&#xff0c;但我还是整天没事瞎折腾。因为一些功能在Halo上不太好实…

MES管理系统计划排产有哪些重要作用

在当今制造业中&#xff0c;MES管理系统解决方案已经成为提高生产效率、优化资源利用以及提升企业整体运营水平的关键工具。尤其是在生产计划排产这一核心环节&#xff0c;MES管理系统发挥着无可替代的作用。通过精准的计划和调度&#xff0c;MES管理系统帮助企业实现生产过程的…

SAP EXCEL上传如何实现指定读取某一个sheet页(ALSM_EXCEL_TO_INTERNAL_TABLE)

如何读取指定的EXCEL sheet 页签&#xff0c;比如要读取下图中第二个输出sheet页签 具体实现方法如下&#xff1a; 拷贝标准的函数ALSM_EXCEL_TO_INTERNAL_TABLE封装成一个自定义函数ZCALSM_EXCEL_TO_INTERNAL_TABLE 在自定义函数导入参数页签新增一个参数SHEET_NAME 在源代码…

【word密码】Word 文档设置了只读为什么还能编辑?

有些朋友可能会遇到这种疑问&#xff0c;为什么我的Word文档设置了只读模式&#xff0c;还是可以编辑的&#xff0c;这是什么原因呢&#xff1f; 其实是因为大部分的只读模式&#xff0c;设置完成之后都是可以编辑的&#xff0c;但是当我们进行保存的时候就会发现&#xff0c;…

【边缘计算】TA的基本概念,以及TA的挑战和机遇

大家好&#xff0c;我是全栈小5&#xff0c;欢迎阅读文章&#xff01; 此篇是【话题达人】序列文章&#xff0c;这一次的话题是《边缘计算的挑战和机遇》 文章将以博主的角度进行讲述&#xff0c;理解和水平有限&#xff0c;不足之处&#xff0c;望指正。 目录 背景基本概念挑战…

软件游戏提示msvcp140.dll丢失的解决方法,全面分析msvcp140.dll文件

msvcp140.dll是Microsoft Visual C 2015 Redistributable的一部分&#xff0c;它包含了许多用于运行程序的函数和类库。当这个文件丢失或损坏时&#xff0c;依赖于该组件的应用程序可能无法正常启动&#xff0c;系统会弹出错误提示&#xff0c;告知用户找不到msvcp140.dll文件。…

【Linux】糟糕,是心动的感觉——与Linux的初次相遇

初识Linux 导言一、计算机的发展1.1 历史背景1.2 计算机的发明 二、操作系统2.1 什么是操作系统&#xff1f;2.2 操作系统的诞生2.3 操作系统的发展2.3.1 批处理系统的发展2.3.2 分时系统2.3.3 实时系统2.3.4 通用操作系统 2.4 UNIX操作系统2.4.1 UNIX的诞生2.4.2 UNIX的发展 2…

ESXI 本地和虚拟机之间可以自由复制和粘贴

文章目录 ESXI 本地和虚拟机之间可以自由复制和粘贴 ESXI 本地和虚拟机之间可以自由复制和粘贴 web访问esxi&#xff0c;然后&#xff1a; 1、右击新建的虚拟机&#xff0c;确保是在关机状态下&#xff0c;点击编辑设置 2. 找到 虚拟机选项→高级→常规→配置参数 3、点击添加…

Scrapy爬虫在新闻数据提取中的应用

Scrapy是一个强大的爬虫框架&#xff0c;广泛用于从网站上提取结构化数据。下面这段代码是Scrapy爬虫的一个例子&#xff0c;用于从新闻网站上提取和分组新闻数据。 使用场景 在新闻分析和内容聚合的场景中&#xff0c;收集和组织新闻数据是常见需求。例如&#xff0c;如果我…

❤css实用

❤ css实用 渐变色边框&#xff08;Gradient borders方法的汇总 5种-代码可直接下载&#xff09; 资源链接 https://download.csdn.net/download/weixin_43615570/88779950?spm1001.2014.3001.5503 给 border 设置渐变色是很常见的效果&#xff0c;实现这个效果有很多思路 1…

Python requests网络库源码分析(第三篇:通过学习异常模块,了解http协议)

前言 作者在requests包下&#xff0c;定义了exceptions模块&#xff0c;该模块中定义执行http请求过程中常见的错误&#xff0c;熟悉这些错误有助于我们写出健壮的业务程序&#xff0c;同时还能温习http的知识点&#xff0c;本文基于的requests版本为2.27.1 exceptions模块&…

Sectigo多域名通配符证书区别买一年送一个月

Sectigo是国际知名CA认证机构之一&#xff0c;旗下有多种SSL数字证书&#xff0c;如单域名SSL证书、多域名SSL证书、通配符SSL证书、IP证书、多域名通配符SSL证书等。Sectigo旗下的多域名通配符证书有两种&#xff0c;它们在保护的域名类型和数量上存在一定差异&#xff0c;今天…

学校门禁监控,怎么管理更高级?

随着社会的不断发展和科技的飞速进步&#xff0c;安全管理成为各个领域不可或缺的重要环节。在这一安全管理体系中&#xff0c;门禁监控系统扮演着至关重要的角色&#xff0c;为组织、企业和机构提供了先进的、智能化的安全解决方案。 因此&#xff0c;门禁监控系统不仅仅是简单…

Linux简介

Unix的特点&#xff1a; Unix很简洁&#xff0c;Unix只提供几百个系统调用&#xff0c;并且每个调用都有明确的目的。一切皆文件&#xff0c;对数据和对文件都是通过相同的系统调用接口&#xff1a;open&#xff08;&#xff09;&#xff0c;read&#xff08;&#xff09;&…

应用监控 eBPF 版:实现高效协议解析的技术探索

作者&#xff1a;彦鸿 引言 随着 Kuberentes 等云原生技术的飞速发展&#xff0c;带来了研发与运维模式的变革。企业软件架构由单体服务向分布式、微服务演进。随着业务发展&#xff0c;多语言、多框架、多协议的微服务在企业中越来越多&#xff0c;软件架构复杂度越来越高&a…

谈谈 RocketMQ 5.0 分级存储背后一些有挑战的技术优化

作者&#xff1a;斜阳 RocketMQ 5.0 提出了分级存储的新方案&#xff0c;经过数个版本的深度打磨&#xff0c;RocketMQ 的分级存储日渐成熟&#xff0c;并成为降低存储成本的重要特性之一。事实上&#xff0c;几乎所有涉及到存储的产品都会尝试转冷降本&#xff0c;如何针对消…

火灾监测识别摄像机

火灾监测识别摄像机是一种基于视觉识别技术的智能设备&#xff0c;旨在实时监测并识别火灾&#xff0c;及时报警并提供相关数据支持&#xff0c;以提高火灾应急响应和减少火灾灾害损失。 火灾监测识别摄像机利用高清摄像头和先进的图像处理技术&#xff0c;能够对室内和室外环境…