【数据结构】链表详解

 本片要分享的内容是链表,为方便阅读以下为本片目录

目录

1.顺序表的问题及思考

1.链表的遍历

2.头部插入

2.1开辟空间函数分装

3.尾部插入

纠正

4.尾部删除

5.头部删除  

6.数据查找

7.任意位置插入


1.顺序表的问题及思考

上一篇中讲解了顺序表中增删查改的操作,但是如果在非常庞大的项目中只用顺序表来解决问题局限性还是多了一点,我们不妨想想顺序表是否有以下这些问题

1. 中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

随意要如何解这些问题呢?

1.不需要扩容;

2.按需申请释放空间;

3.解决插入和删除需要挪动数据的问题;

顺序表的根源就是一块数组一样的连续的物理空间,所以我们可以再申请时一个一个的申请一小块空间来储存他的信息,不像顺序表申请空间时申请一大块空间。

所以就可以使用链表来解决这样的问题。

   

 前面的文章中我们题到过顺序表的空间是连续的,通过访问下标就可查找到数据, 而单链表的空间是一块一块的不是连续的,所以我们无法通过下标来访问这些数据

所以我们依旧可以通过指针来访问他们的信息。

在链表的下一个空间中存放上一个空间的指针,就像穿串一样将他们全都串联起来;这样就可以访问一个空间的同时也能知道下一个空间的地址。

 我们不妨定义结构体来完成链表的操作

typedef int SLDatatype;
typedef struct SListNode
{
	SLDatatype data;
	struct SListNode* next;
}SLTNode;

先简单的对我们想要操作的数据类型重命名,方便以后修改数据类型;

定义一个结构体,为讲解时操作简便只存放了一个数据,同时要存放上一个空间的地址,所以需要使用结构体指针;

接下来对使用链表进行操作;

1.链表的遍历

 对单链表内容的遍历展现是非常简单的操作,代码如下:

void SLPrint(SLTNode* phead)
{
	SLTNode* cur = phead;//将phead的内容拷贝一份给cur
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

在这里我们先定义一个结构体类型的指针,将phead的内容拷贝一份给cur,就相当于有了两个相同内容的指针;

 (方框内的地址只是随机赋值,无其他意义)

接下来就要使用结构体类型的指针去操作内容,cur->指向了结构体的内容便可以访问其内容,再用打印函数将其输出;

也就是说cur->next就是下一个节点的位置,每访问一次下一个节点,他的位置就会被保存到cur中去,循环往复

2.头部插入

头部插入需要主义的是只需要在链表的起始位置做文章即可,代码如下(注意代码有误)

//头部插入
void SLPushFront(SLTNode* phead, SLDatatype x)
{
    //开辟空间
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}

    //赋值
	newnode->data = x;
	newnode->next = NULL;

    //链接
	newnode->next = phead;
	phead = newnode;
}

 参数有两个,一个是地址,另一个是像添加的数据的内容;

既然是要添加,那必须使用malloc来扩容,同时要给malloc扩容的空间进行判断,分享过很多次这里不再过多赘述;

再通过结构体指针访问结构体内容改变值;

最后的newnode是一个指针变量,通过这个指针变量给next赋值,既然是头部插入所以原先链表的第一个数据就变成了第二个,所以next的值就是原先链表数据的地址。这里不难看出phead就是原先链表中第一个数据的地址,所以将其赋给next;

同时newnode又成为了插入数据后链表的第一个数据,所以又要将newnode的值赋给phead,以便于下一次使用头部插入;

我们将代码使用到main函数中运行一下

我们发现并没有按我们预期的那样输出值 

原因是在逻辑上还存在一些问题

SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));

 我们发现newnode是一个局部变量,出了作用域后会自动销毁,同样的phead是一个形参,使用后也会被销毁掉,所以无法找到链表的第一个节点,所以我们在运行时还得在main函数中保留一个指针

SLTNode* list = NULL;

来确保链表可以找到第一个节点

那既然在main函数中的局部变量是一个指针

我们不妨从之前的两数交换的函数来入手观察以上代码的问题

 我们将两数交换写成代码的形式时会将形式参数写成实参的地址,在函数中解应用从而达到两数交换的功能,由此我们可以得到的结论是想要操作某个内容,就用函数传这个内容的地址。

 当我们想要改变指针所指向的内容时,我们发现调用函数无法改变内容,

所以这里还是用上面所说的结论,想要操作某个内容,就用函数传这个内容的地址;

这里我们想要操作两个指针,就要传这个指针的地址,注意是指针的地址,所以我们必须使用二级指针来解决问题。

以下为正确代码

void SLPushFront(SLTNode** pphead, SLDatatype x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;


	newnode->next = *pphead;
	*pphead = newnode;
}

这样我们再带入到主函数中使用我们所写的函数

 这样就可以达到我们预期中的效果了

2.1开辟空间函数分装

我们发现在之前所写的代码中都需要使用malloc开辟空间、判断开辟成功、最后将节点置空这些操作,所以我们不妨将这些操作总写成一个函数以便于我们使用

SLTNode* BuyLTNode(SLDatatype x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
}

可以看到在这个函数中有开辟空间,检查空间,将空间置空的操作,在接下来的插入操作中我们使用以上函数; 

3.尾部插入

尾部插入相对头部插入相对有一点麻烦,因为需要找上一个节点的尾巴,在理解上可能会有一些难度,代码如下(以下代码有误)

void SLPuchBack(SLTNode* phead, SLDatatype x)
{
	SLTNode* tail = phead;
	while (tail->next != NULL)
	{
		tail = tail->next;

	}
	SLTNode* newnode = BuyLTNode(x);
	tail->next = newnode;
}

首先我们要再定义一个结构体变量的指针,将phead的值赋给tail,我们只需要操作tail就等于操作了phead;

接下来我们需要查找整个链表的尾部,所以可以看到以上代码的判断条件,当tail中的next变量不为空时,tail就指向下一个节点,也就是说next为空时,循环就结束;

在接下来再重新创建一个结构体指针newnode,他是我们尾插所要存放数据的空间,我们使用BuyLTNode函数对他进行初始化;

因为newnode是一个新的结构体指针,next也是我们最初所创建的结构体指针,所以最后将newnode结构体指针再赋给tail中的next即可,这样就将新的空间和之前尾部的空间连接起来;

要注意的是我们想要和链表中的上一个节点和下一个节点形成链接的时候,就必须要操作结构体指针变量中的next,因为next也是结构体指针变量,他存放的是的下一个节点的地址,这样才能和下一个节点连接起来。

纠正

我们还需要考虑的一点就是当这个链表一开始就没有数据该怎么办呢?很显然上面的代码没有考虑到这种情况;当链表为空的时候,以上代码就不能插入数据了;

 我们会发现程序会崩溃掉

所以我们继续来解决这种情况

我们发现plist是一个指针变量,还是之前所说过的,我们想要改变数据,就操作这个数据的地址;

很显然我们传参传的是指针的地址,所以我们就要操作指针的地址,所以在编写函数时就要使用二级指针来操作指针的地址;

代码如下

//尾部插入
void SLPuchBack(SLTNode** pphead, SLDatatype x)
{
	SLTNode* newnode = BuyLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;

		}
		tail->next = newnode;
	}
}

 这次我们考虑的情况是如果链表中一个数据都没有时,我们就需要先申请一个空间来存放我们的数据,如以上代码的if语句所示;

接下来如果不是空链表就依旧按照原来的代码进行插入然后链接,应该不难看懂;

这里还需要注意的是我们只有在第一次,也就是链表中为空时运用了二级指针来添加第一块空间,后面的添加都只需要操作指针中的next指针即可,当链表为空时也就只使用一次二级指针,再向后添加数据时都是用一级指针;

4.尾部删除

删除操作要注意的还是两个指针的问题,从如果操作不当可能就会出现野指针从而使程序报错;

我们先观察以下错误的写法

void SLPopBack(SLTNode** pphead, SLDatatype x)
{
	SLTNode* tail = *pphead;
	while (tail->next !=NULL)
	{
		tail = tail->next;
	}
	free(tail);
	tail = NULL;
}

这串代码代码看上去没什么问题,判断,free释放基本上没什么问题

但是要深入研究就会发现和正常的代码简直就是天差地别;

之前说到过删除就是将这个节点置空即可,但是我们要注意,当我们将节点置空的时候,指向这个节点的指针就会成为一个野指针,所以我们要想办法规避野指针。

我们不妨再创建一个指针变量prev

SLTNode* prev = NULL;

我们可以再上面看到tail = tail->next;这串代码的意思就是让tail存放下一个指针的地址,所以我们不妨让prev来存放tail,让tail每走一步就让prev存放一次tail的值

 

 tail和prev一前一后,tail每走一步就让prev存放一次tail的值;

当tail是我们想要删除的数据时我们就可以让prev中的next置空,从而达到删除的效果,也规避了上述野指针的出现。

但是还要注意的是如果链表中只有一个数据该怎么办呢?链表为空该怎么办呢?只有一个节点我们就无法找到前一个节点的next并将其置空;所以我们还是用判断条件和二级指针来解决问题。

那代码如下

//尾部删除
void SLPopBack(SLTNode** pphead)
{
	//暴力检查指针是否为空
	assert(*pphead); 

	//判断是否只有一个数据
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//判断多个数据 
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}

} 

我们可以带到主函数中应用

 

 可以看到末尾的4被删除掉了

我们再试试链表中没有数据的情况下

 可以看到指向链表中第一个数据的指针为空时,assert断言就发挥了作用,程序出现错误。

5.头部删除  

同样的删除还需要添加上对空链表的判断和链表中是否只有一个数据的判断;

具体代码如下

//头部删除
void SLPopFront(SLTNode** pphead)
{
	//暴力检查指针是否为空
	assert(*pphead);

	//判断是否只有一个数据
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//判断多个数据 
	else
	{
		SLTNode* del = *pphead;
		*pphead = del->next;
		free(del);
	}
}

 判断多个数据的时候我们只需要在创建一个新的结构体变量将*pphead保存,也就是将plist保存,然后将下一个节点的地址赋值给*pphead,让下一个节点成为最开始的节点,最后再释放掉没有操作前的del即可完成头部删除的操作; 

6.数据查找

链表数据的查找不算太难,我们只需要遍历链表中的数据即可

//数据查找
SLTNode* SLTFind(SLTNode* phead, SLDatatype x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL; 
}

重新定义一个结构体指针cur,将phead的值赋给cur,我们对cur操作即可;

利用while循环对结构体进行遍历,如果结构体中data等于我们想要找的数x,返回x即可;

找不到就返回空;

当我们要在主函数中使用它时要注意他的返回类型是一个指针类型,所以要在主函数中定义一个指针类型来接受它;

 

既然是找到了这个数并且返回了他的指针,那么我可以通过这个函数对他的值进行修改(如上图),

运行的效果如下

 可以看到我们将3查找到后将3改成了30;

7.任意位置插入

这里需要和顺序表中插入保持一致,需要在输入的数前一个位置插入;

同样我们还要使用assert来断言pos的位置和phead位置是否为空;

具体代码如下

//任意位置插入
void SLInsert(SLTNode** pphead, SLTNode* pos,SLDatatype x)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLPuchBack(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}


		SLTNode* newnode= BuyLTNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

当我们想要插入数字的位置等于链表起始位置的时候,我们就尾插来实现插入数据;

其他的情况时我们需要重新定义一个结构体指针将plist的地址拷贝给新定义的指针;

接下来就都使用插入时的常规操作,通过条件判断能否使prev和下一个结点进行连接;

最后就是将指针的内容交换,实现链表中添加数据的操作

 
以上就是关于单链表的增删查改的内容,如果对你有所帮助还请三联支持一下,感谢您的阅读。

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

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

相关文章

从源码全面解析LinkedBlockingQueue的来龙去脉

一、引言 并发编程在互联网技术使用如此广泛,几乎所有的后端技术面试官都要在并发编程的使用和原理方面对小伙伴们进行 360 的刁难。 二、使用 对于阻塞队列,想必大家应该都不陌生,我们这里简单的介绍一下,对于 Java 里面的阻塞…

Python | 基于LendingClub数据的分类预测研究Part01——问题重述+特征选择+算法对比

欢迎交流学习~~ 专栏: 机器学习&深度学习 本文利用Python对数据集进行数据分析,并用多种机器学习算法进行分类预测。 具体文章和数据集可以见我所发布的资源:发布的资源 Python | 基于LendingClub数据的分类预测研究Part01——问题重述特…

在.NET Core中正确使用HttpClient的方式

HttpClient 是 .NET Framework、.NET Core 或 .NET 5以上版本中的一个类,用于向 Web API 发送 HTTP 请求并接收响应。它提供了一些简单易用的方法,如 GET、POST、PUT 和 DELETE,可以很容易地构造和发送 HTTP 请求,并处理响应数据。…

【Excel统计分析插件】上海道宁为您提供统计分析、数据可视化和建模软件——Analyse-it

Analyse-it是Microsoft Excel中的 统计分析插件 它为Microsoft Excel带来了 易于使用的统计软件 Analyse-it在软件中 引入了一些新的创新统计分析 Analyse-it与 许多Excel加载项开发人员不同 使用完善的软件开发和QA实践 包括单元/集成/系统测试 敏捷开发、代码审查 …

虹科案例|虹科Micronor光纤传感器,实现核磁共振新应用!

PART 1 背景介绍 光纤传感器已成为推动MRI最新功能套件升级和新MRI设备设计背后的关键技术。将患者的某些活动与MRI成像系统同步是越来越受重视的需求。磁场强度随着每一代的发展而增大,因此,组件的电磁透明度在每一代和新应用中变得更加重要。 光学传…

《Netty》从零开始学netty源码(四十六)之PooledByteBuf

PooledByteBuf Netty中一大块内存块PoolChunk默认大小为4MB,为了尽可能充分利用内存会将它切成很多块PooledByteBuf,PooledByteBuf的类关系图如下: PooledUnsafeDirectByteBuf与PooledUnsafeHeapByteBuf直接暴露对象的底层地址。 PooledByt…

【英语】100个句子记完7000个托福单词

其实主要的7000词其实是在主题归纳里面,不过过一遍100个句子也挺好的,反正也不多。 文章目录 Sentence 01Sentence 02Sentence 03Sentence 04Sentence 05Sentence 06Sentence 07Sentence 08Sentence 09Sentence 10Sentence 11Sentence 12Sentence 13Sent…

数据分析中常见标准的参考文献

做数据分析过程中,有些分析法方法的标准随便一搜就能找到,不管是口口相传还是默认,大家都按那样的标准做了。日常分析不细究出处还可以,但是正式的学术论文你需要为你写下的每一句话负责,每一个判断标准都应该有参考文…

Docker | 解决docker 容器中csv文件乱码的情况

问题描述:在Ubuntu docker容器中,打开.csv文件时显示乱码 问题如图 错误分析: 用locale查看所用容器支持的字符集 从输出可以看到,系统使用的是POSIX字符集,POSIX字符集是不支持中韩文的,而UTF-8是支持中…

刷题4.28

1、 开闭原则软件实体(模块,类,方法等)应该对扩展开放,对修改关闭,即在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关…

vue之--使用TypeScript

搭配 TypeScript 使用 Vue​ 像 TypeScript 这样的类型系统可以在编译时通过静态分析检测出很多常见错误。这减少了生产环境中的运行时错误,也让我们在重构大型项目的时候更有信心。通过 IDE 中基于类型的自动补全,TypeScript 还改善了开发体验和效率。…

【Android Framework (八) 】- Service

文章目录 知识回顾启动第一个流程initZygote的流程system_serverServiceManagerBinderLauncher的启动AMS 前言源码分析1.startService2.bindService 拓展知识1:Service的两种启动方式对Service生命周期有什么影响?2:Service的启动流程3:Service的onStartCommand返回…

nginx(七十二)nginx中与cookie相关的细节探讨

背景知识铺垫 一 nginx中与cookie相关 ① Cookie请求头内容回顾 cookie的形式和属性 ② nginx获取cookie值的两种方法 1) $http_cookie -->获取Cookie请求头"所有值"2) $COOKIE_flag -->获取Cookie请求头的"某个key"[1]、脱敏场景在日志中只…

榜上有名 | 创宇盾荣登“2023 IT市场权威榜单”!

4月20日,已连续成功举办23届的IT市场年会在北京举行,作为权威咨询机构赛迪主办,中国IT业界延续时间最长的年度盛会之一,“2023 IT市场年会”隆重发布重磅权威榜单。 创宇盾作为云防护领域专业防护产品,在国家经济产业…

零成本教你部署一个ChatGPT网站

📋 个人简介 💖 作者简介:大家好,我是阿牛,全栈领域优质创作者。😜📝 个人主页:馆主阿牛🔥🎉 支持我:点赞👍收藏⭐️留言&#x1f4d…

为何电商这么难做…...你是否忽略了这个问题?

物流时效是影响买家体验的重要环节,物流服务优劣也是买家网上购物时的重要参考依据。但电商企业对于快递公司的时效承诺、服务质量基本处于被动接受的状况,直到买家投诉才知道快递公司服务缺失,若买家不投诉也没法主动知道大量的订单是否按约…

Excel技能之实用技巧,高手私藏

今天来讲一下Excel技巧,工作常用,高手私藏。能帮到你是我最大的荣幸。 与其加班熬夜赶进度,不如下班学习提效率。能力有成长,效率提上去,自然不用加班。 消化吸收,工作中立马使用,感觉真不错。…

随手记录:Livox 时间戳修改为ROS时间戳

参考与前言 传感器类型:Livox-Mid70 参考链接:Ubuntu20.04系统安装Livox ROS Driver 官方驱动:https://github.com/Livox-SDK/livox_ros_driver 碎碎念:之所以要改成rostime主要是 提取pcd的时候发现这个timestamp 300.xxx 打…

史上最全Maven教程(三)

文章目录 🔥Maven工程测试_Junit使用步骤🔥Maven工程测试_Junit结果判定🔥Maven工程测试_Before、After🔥依赖冲突调解_最短路径优先原则🔥依赖冲突调解_最先声明原则🔥依赖冲突调解_排除依赖、锁定版本 &a…

ByteHouse云数仓版查询性能优化和MySQL生态完善

ByteHouse云数仓版是字节跳动数据平台团队在复用开源 ClickHouse runtime 的基础上,基于云原生架构重构设计,并新增和优化了大量功能。在字节内部,ByteHouse被广泛用于各类实时分析领域,最大的一个集群规模大于2400节点&#xff0…