C语言----单链表的实现

news2024/11/23 14:03:07

前面向大家介绍了顺序表以及它的实现,今天我们再来向大家介绍链表中的单链表。

1.链表的概念和结构

1.1 链表的概念

链表是一种在物理结构上非连续,非顺序的一种存储结构。链表中的数据的逻辑结构是由链表中的指针链接起来的。

1.2 链表的结构

链表的结构与火车相似。

3e2efeadaa264c3b85afa3c54c291c68.jpeg

fec2696487ea4e3dba19cddc85e1a605.jpeg

火车是由一节一节的车厢构成的,并且各个车厢之间是相互独立的,且每个车厢都有属于自己的锁。假设火车上车厢的门都是锁上的状态,每个门都要对应的锁来开门,那我们如何快速的从第一个车厢走到最后一个车箱呢?

答案很简单,我们只要把下一节的车厢的钥匙放在上一节车厢就行了。

链表也是如此,车厢对应到链表中就是节点

所以链表是由一个个节点组成的,每个节点由存储的数据和指向下一个节点的指针组成的。

为什么需要指针呢?

因为链表在物理结构上是不连续的,由于连表中的节点的地址是由计算机随机分配的,我们并不能清楚的知道各个节点的具体位置,这时候就需要指针了。通过指针我们就能知道每个节点的位置。

53a3ff77d2f04c1f8606ab16adb7d5fc.jpeg

上图就是一个单链表的结构,plist是一个指向第一个节点的指针,往后看会发现,每一个节点都会包含了下一个节点的指针。

2.单链表的实现 

单链表的实现,我们依然通过三个文件来实现,为SList.h,SList.c和test.c

2.1 单链表的创建

我们通过前面顺序表就可以很快写出以下代码

typedef int SLDataType;
struct SListNode
{
	SLDataType data;
	struct SListNode* next;
}SList;

2.2 单链表的初始化

链表是由一个一个的节点组成的,所以我们要为节点申请空间,会用到malloc函数。

void test01()
{
	//手动初始化
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;
	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 1;
	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 1;
	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 1;
	
	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;
	SLTNode* plist = node1;
}

为了方便观察,我们先写一个打印链表的函数。

SLPrint(SLTNode* phead)
{
	SLTNode* pur = phead;
	while (pur)
	{
		printf("%d->", pur->data);
		pur = pur->next;
	}
	printf("NULL\n");
}

解释以上代码

pur是一个指向第一个节点的指针,我们知道最后一个节点中的指针为NULL,pur在不断的变换为next的值,直到pur的值为NULL就跳出循环。

f0d6b35670ad48bda8dd6cf097971e35.png

运行代码

714f26bb7f334b37a28066987501ba6a.png

我们就发现单链表初始化成功了。

但是上面的代码是我们动手来实现链表的初始化的,但这样写代码的效率就会降低,我们一般都会通过函数来实现,这就涉及到了链表数据的插入。

2.3 数据的插入

数据的插入方式我们分为尾插和头插的两种。

2.3.1 尾插

尾插,顾名思义就是从链表中的尾部插入一个新的节点。

c5cf0898e9de45b083801c6157360b55.png

上图就是尾插的形式。

思路分析

既然我们要插入一个新的节点,我们就要为新的节点申请空间,为了方便,我们同样把申请空间的操作包装成一个函数。

//申请空间
SLTNode* SLBuySpace(SLDataType x)
{
	SLTNode* new = (SLTNode*)malloc(sizeof(SLTNode));
	//判断空间是否申请成功
	if (new == NULL)
	{
		perror("malloc fail");
		exit(1);//退出程序
	}
	//到这空间申请成功
	new->data = x;
	new->next = NULL;
	return new;
}

接着,既然要实现尾插,我们就要找到链表的尾巴,然后才能插上新的节点。

//找尾巴
SLTNode* ptail = *pphead;
while (ptail->next)
{
	ptail = ptail->next;
}
ptail->next = newnode;

尾插的总代码

//尾插
void SLPushBack(SLTNode** pphead, SLDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLBuySpace(x);
	if (*pphead == NULL)
	{
		//链表为空
		*pphead = newnode;
	}
	else
	{
		//链表不为空
		//找尾巴
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
	

}

需要注意的是,我们这里的形参是一个二级指针,因为我们要将原来phead指针指向的内容进行改变,如果我们单独将指针的值传过来,通过前面的学习,我们传值时,形参的改变是不会影响实参的,所以我们要将指针的地址传过来,通过地址修改实参的值。 

13f8f7097aaa4d2ab67af3c839aec2dc.png

运行代码

fc28d4e48bfc40d492402fa3b782493e.png

还需注意的是,我们要将链表为空和不为空分为两种情况处理,如果我们只考虑到链表不为空的情况,则当我们一开始处理的链表为空时,就不会进入循环,为空,ptial->next就无法进行解引用。 

2.3.1 头插 

头插,也就是将一个新的节点插入链表的头部,使新的节点称为第一个节点。

473e5bd44d8647b2b9bf9f5aad03b5b9.png

代码实现

//头插
void SLPushHead(SLTNode** pphead, SLDataType x)
{
	assert(pphead);
	//为新节点申请空间
	SLTNode* newnode = SLBuySpace(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

头插的代码很简单,但是最后要让newnode成为新的phead,不要漏掉*pphead=newnode。

运行代码

b159301993f147448d07d6ae5585bdd4.png

2.4 数据的删除

2.4.1  尾删

尾删就是将链表中的最后一个节点删除掉。

思路分析

尾删我们就要找到链表的尾巴,并将其释放掉,但注意的是,当我们将最后一个节点释放掉之后,前一个节点中的next就会变成野指针,所以我们也要找到最后一个节点的前一个节点,并将其next指针赋值为NULL。

尾删前如下图

ffdee85191284d88ae196b6577c89b21.png

尾删后如下图

fbcd01006e1d4f818cae9ada4764556b.png

代码实现

void SLPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);//链表不能为空
	SLTNode* prev = *pphead;
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	//这里,prev和ptail找到
	free(ptail);
	prev->next = NULL;
}

运行代码

de8015dcc63d4089be289389215dc4e3.png

2.4.2 头删

 头删就是将链表中的第一个节点删点。

代码实现

//头删
void SLPopHead(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;//将下个节点变为新的节点
}

我们需要把下一个节点变为新的头节点,所以我们创建一个next先将下一个节点的地址存储起来。 

2.5 查找数据

查找数据很简单,只需遍历链表,并返回存储要查询数据节点的地址,如没由,就返回NULL。

代码实现

SLTNode* SLDataFind(SLTNode* phead, SLDataType x)
{
	assert(phead);
	SLTNode* pcur = phead;
    //遍历链表
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

运行代码

e83a55ba8c2847a7a1ae0eaf47da26db.png

2.6  在指定位置之前插入数据

在指定位置之前插入数据,会影响到插入数据前一个节点的·指针,所以我们要找到插入位置的前一个节点。如下图

90db917b4b7e4ad39e02d43506eab8ea.png

代码实现

void SLAddPos(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	SLTNode* newnode = SLBuySpace(x);
	if (*pphead == pos)
	{
		//链表为空
		//头插
		SLPushHead(pphead, x);
	}
	else
	{
		//链表不为空
		//找位置pos前面的节点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}	
}

2.7 在指定位置之后插入数据 

 在指定位置之后插入数据就很简单,这个操作会影响插入位置的后一个指针,因为我们可以通过插入位置来找到插入位置的后一个节点,不在需要遍历链表。

0f08b6b2f0f24a11892814e10541bfa0.png

我们只需将pos->next指向newnode,让newnode->next指向pos->next。

代码实现

void SLAddBack(SLTNode* pos, SLDataType x)
{
	assert(pos);
	//为插入节点申请空间
	SLTNode* newnode = SLBuySpace(x);
	SLTNode* next = pos->next;
	pos->next = newnode;
	newnode->next = next;
}

这里我们要先将pos->next保存下来,因为后面的pos->next会发生改变,而我们要让newnode->next指向原来的pos->next;

2.8 删除pos节点的数据 

当我们删除pos节点时,会影响到pos前后两个节点的指针,所以,我们首先要找到pos前后的两个节点。

4a2347ffd63c4bb682748191037eb922.png

代码实现

void SLErasePos(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	if (pos == *pphead)
	{
		//只有一个节点
		//头删
		SLPopHead(pphead);
	}
	else
	{
		SLTNode* next = pos->next;
		//找prev
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		free(pos);
		pos = NULL;
		prev->next = next;
	}

}

注意事项:我们要先将pos->next的值先存储起来,因为前面的pos就会被释放掉了,最后就找不到pos->next了。

我们还要分情况讨论,当链表中只有一个节点时,那就是头删操作了,直接调用头删的函数就行了。

运行代码

7afd31b9624149cdab671233180cecb6.png

2.9 删除pos后的节点

思路分析

既然要删除pos后的节点,首先链表就不能为空,pos->next也不能为空。

0e995b8904064e49a68264cabefe57c6.png

注意,将pos后面的节点释放掉了之后,此时pos->next就是野指针来,要注意将pos->next置为空。

代码实现

void SLEraseAfter(SLTNode* pos) //pos->data==1
{
	assert(pos && pos->next);
	//要删除的节点
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

d309232e88b347fea5087b5a01da7435.png

2.10 销毁链表

销毁链表就一个一个销毁就行了。

代码实现

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

感谢观看。 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Dart】双问号表达式报错的解决方案

最近准备学习一下Flutter,现从Dart开始。 Dart ??运算符报错的解决方案 报错代码如下 main() {int a;int b a ?? 123;print(b); }报错表现如下 _D05.8%20%E5%8F%8C%E9%97%AE%E5%8F%B7%E8%BF%90%E7%AE%97%E7%AC%A6.dart:3:11: Error: Non-nullable variable …

Lagent AgentLego 智能体应用搭建-作业六

本次课程由Lagent&AgentLego 核心贡献者樊奇老师讲解【Lagent & AgentLego 智能体应用搭建】课程。分别是: Agent 理论及 Lagent&AgentLego 开源产品介绍Lagent 调用已有 Arxiv 论文搜索工具实战Lagent 新增自定义工具实战(以查询天气的工具…

您的计算机已被rmallox勒索病毒感染?恢复您的数据的方法在这里!

引言: 在当今数字化时代,网络安全问题日益突出,其中勒索病毒作为一种新型的网络威胁,正逐渐引起人们的广泛关注。其中,.rmallox勒索病毒作为近期出现的一种新型恶意软件,给个人和企业带来了巨大的经济损失…

电机入门1

文章目录 122.12.22.3 33.13.23.33.4 1 2 2.1 电机板 驱动板电机分类 驱动器分类 转速 转向扭矩定时器 ADC 2.2 PID 自动控制 的核心闭环控制算是 PID的应用 2.3 无刷电机用的 可大大提高其控制效率 和控制精度 3 开发板的IO 电流太小了 20~25ma 电机要A 驱动板 信号放大没舵…

html表格(详解网页表格的制作)

一、表格介绍 HTML 表格由 <table> 标签来定义。 HTML 表格是一种用于展示结构化数据的标记语言元素。 每个表格均有若干行&#xff08;由 <tr> 标签定义&#xff09;&#xff0c;每行被分割为若干单元格&#xff08;由 <td> 标签定义&#xff09;&#x…

用卷积网络对城市住区进行分类

这将是解释我的人工智能硕士最终项目的几篇文章中的第一篇&#xff0c;我想在其中详细解释从项目的想法到结论&#xff0c;我将在其中展示给定解决方案的所有代码。 总体思路 城市扩张地图集 https://www.lincolninst.edu/es/publications/books/atlas-urban-expansion 项…

【linux】chmod权限开放(整个文件夹)

文章目录 起因权限查看权限修改 失败权限修改成功 起因 想要共享conda环境给同事&#xff0c;发现同事没权限。 权限查看 ls #查看当前目录 ls -l # 查看当前目录的东西和权限正常情况下是显示 三个rwx分别属于user&#xff0c;group&#xff0c;others 前面第一个rwx 是针…

【斐波那契】原来困扰多年的生兔子问题竟然能够轻松拿捏...万能公式法...

上篇文章我们讲解了「矩阵快速幂」技巧&#xff0c;通过快速幂极大的优化了 斐波那契数列 的求解问题。并且通过分析知道了 系数矩阵 是解决问题的关键。 本文我们继续深化对于 系数矩阵 的求解&#xff0c;介绍一种通用方法&#xff0c;一举解决所有 斐波那契及变种类型 的问…

路由器使用docker安装mysql和redis服务

路由器使用docker安装mysql和redis服务 1.先在路由器中开启docker功能 &#xff08;需要u盘 或者 移动硬盘&#xff09; 2. docker 管理地址 :http://192.168.0.1:11180/#/ 3. 拉取镜像 4. mysql容器参数设置 MYSQL_ROOT_PASSWORD 5. redis 容器设置 开发经常需要用到 &…

【工具-PyCharm】

工具-PyCharm ■ PyCharm-简介■ PyCharm-安装■ PyCharm-使用■ 修改主题■ 设置字体■ 代码模板■ 解释器配置■ 文件默认编码■ 快捷键■ 折叠■ 移动■ 注释■ 编辑■ 删除■ 查看■ 缩进■ 替换 ■ PyCharm-简介 官方下载地址 Professional&#xff1a;专业版&#xff0…

贪吃蛇撞墙功能的实现 和自动行走刷新地图 -- 第三十天

1.撞墙 1.1最初的头和尾指针要置为空&#xff0c;不然是野指针 1.2 在增加和删除节点后&#xff0c;判断是否撞墙&#xff0c;撞墙则初始话蛇 1.3在撞墙后初始化蛇&#xff0c;如果头不为空就撞墙&#xff0c;得定义临时指针指向头&#xff0c;释放头节点 2.自动刷新地图 2.1…

EMP.DLL是什么文件?EMP.DLL文件缺失怎么解决

在深入探讨计算机世界的微观结构时&#xff0c;我们会发现诸多支撑着软件正常运行的关键组件——动态链接库文件。其中&#xff0c;EMP.dll便是一个颇具代表性的例子。本文旨在全面解析EMP.dll的本质属性、功能作用以及当它丢失时可能带来的问题&#xff0c;结合实践经验&#…

9节点牛拉法matlab

潮流计算程序matlab 牛拉法 采用matlab对9节点进行潮流计算&#xff0c;采用牛拉法&#xff0c;程序运行可靠。

白平衡简介

文章目录 白平衡的概念白平衡的调节常见的白平衡模式 白平衡的概念 白平衡是指摄影、摄像和显示技术中的一项重要概念&#xff0c;用于调节图像中的白色或中性灰色的色彩&#xff0c;使其看起来在不同光源条件下都是准确的白色或灰色。白平衡的主要目的是确保图像的色彩准确性…

配置jupyter的启动路径

jupyter的安装参考&#xff1a;python环境安装jupyter-CSDN博客 1&#xff0c;背景 继上一篇python环境安装jupyter&#xff0c;里面有一个问题&#xff0c;就是启动jupyter&#xff08;命令jupyter notebook&#xff09;之后&#xff0c;页面默认显示的是启动时候的路径。 …

Synchronized关键字的深入分析

一、引言 在多线程编程中&#xff0c;正确地管理并发是确保程序正确运行的关键。Java提供了多种同步工具&#xff0c;其中synchronized关键字是最基本且最常用的同步机制之一。本文旨在深入解析synchronized的实现原理&#xff0c;探讨其在不同应用场景中的使用&#xff0c;并…

vue3第二十五节(h()函数的应用)

1、前言&#xff1a; 为什么vue 中已经有 template 模板语法&#xff0c;以及JSX了&#xff0c;还要使用 h()渲染函数&#xff1b; vue 中选择默认使用template 静态模板分析&#xff0c;有利于DMO性能的提升&#xff0c;而且更接近真实的HTML&#xff0c;便于开发设计人员理…

工业测径仪的应用场景和可靠性判断

关键字:线缆测径仪,圆棒测径仪,圆管测径仪,金属棒管测径仪,工业测径仪,智能测径仪 智能测径仪主要应用于以下领域&#xff1a; 金属加工&#xff1a;测量金属线材、棒材、管材等的直径。线缆制造&#xff1a;检测电线、电缆的直径。塑料管材生产&#xff1a;监控塑料管材的外…

BGP的路径属性

路径属性 l每条BGP路由都拥有多个的路径属性&#xff0c;有些是必须携带的&#xff0c;有些是可选添加的 lBGP的路径属性将影响最优路由的选择 lBGP路径属性是描述路由的一组参数&#xff0c;BGP根据路由的属性选择最佳路由&#xff0c;可以人为置值&#xff0c;以便执行路由…

第十五届蓝桥杯省赛第二场C/C++B组E题【遗迹】题解

解题思路 错解 贪心&#xff1a;每次都移动至当前最近的对应方块上。 反例&#xff1a; s s s abxac t t t abac 贪心结果&#xff08;下标&#xff09; 0 → 1 → 0 → 4 0 \rightarrow 1 \rightarrow 0 \rightarrow 4 0→1→0→4&#xff0c;答案为 5 5 5。 正确结…