C语言单链表

1. 单链表的概念和结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表 中的指针链接次序实现的 。

链表与顺序表都属于线性表,顺序表在物理存储结构上是线性的,但是链表在物理存储结构上却是非线性的,它是由一个个的结点所构成。

一个个结点通过地址链接在一起,所以这种逻辑上的线性存储结构就叫做链表。

准确来说,结点之间只有逻辑上的关系,链表只是假想出来的对这些相互之间有联系的结点的统称。

单链表的“单”意味着每个结点除了存储自己的要存储的数据之外,就只存储了另外一个结点的地址(其逻辑上,下一个结点的地址)。

每个结点并没有自己的名字或直接被查找到的方式,他们只能被上一个元素存储的指针所找到。

其结构体定义如下:

typedef int SLTDataType;//要存储的数据的类型,此处是int

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

其中,data表示要存的数据,next是指向下一个结点的指针。 

在之后的学习中,我们还会遇到双向链表,循环链表,十字链表等。

稀疏矩阵的链式存储结构:十字链表-CSDN博客

既然这些结点在逻辑上被视作一个整体,那么要如何表示这个整体呢?

由于每个结点中存储了下一个结点的地址,所以我们只要找到第一个结点,就可以找到之后所有被链接起来的结点。

由于这些结点并没有自己的名字,所以我们会单独定义一个头结点指针plist来指向第一个结点。

这样,通过plist我们就可以遍历整个链表,通常就将plist当作链表这个整体。

这个其实不难理解,类比数组即可:

数组名既是首元素地址,又代表了整个数组;plist既是首元素地址,又代表了整个链表。

2. 对单链表进行操作用到的函数声明

//打印
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* phead, 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 SListDesTroy(SLTNode** pphead);

其中,打印链表的函数需要根据具体情况来实现,打印完一个结点的数据再打印下一个,直到某个元素的next指针为NULL为止,本文对这个函数不做过多解释。

由于每个插入函数在插入结点时都需要申请一个新的结点来插入,所以除了上面这些函数,我们在实现链表的文件中还会额外定义一个函数来进行结点的申请:

//创建新结点
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

3. 插入函数

3.1 尾插

申请新结点,找到链表中的尾结点,使尾结点的next指针指向新的结点即可。

注意,当我们的头结点指针为NULL时(链表中没有元素),我们需要让头结点指向新申请的结点,此时需要修改头结点指针的值。

所以,我们在传入数据时,传入的是头结点指针的地址。

在函数中,要修改实参的值就需要传入实参的地址。

我们要修改头结点指针的值就要传入头结点指针的地址,即一个二级指针。

接下来的,传入了二级指针的函数也是由于在某些情况下需要更改头结点指针的值。 

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next != NULL)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

3.2 头插

申请新的结点,然后将链表原本的头结点链接在新结点之后,再让头结点指针指向新结点即可。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);

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

3.3 在指定位置之前插入

在指定位置之前插入,需要使该位置之前原本的结点的next指针指向新结点,再让新结点的next指针指向该位置的结点。

由于在单链表中,我们只能查找到某个结点的下一个结点,所以,在某个结点之前插入就需要遍历链表,来找到该位置之前的那个结点。

这也是单链表的一个缺陷。

在某个位置前插入,那么链表中一定要有结点才能插入,所以pphead和*pphead都不能为NULL。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = SLTBuyNode(x);
		newnode->data = x;
		newnode->next = prev->next;
		prev->next = newnode;
	}
}

3.4 在指定位置之后插入

先让该位置的下一个结点接在新结点之后,再让新结点接在该位置之后。

注意,这两步次序不能交换,如果先进行第二步,则不能通过该位置结点的next指针找到原本的下一个结点。

//指定位置之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

4. 删除函数

4.1 尾删

找到尾结点,再将其释放即可,当然,在释放掉尾结点之后,需要将前一个结点的next指针置为NULL。

但我们无法通过尾结点来找到前一个结点,所以每次让ptail指针指向下一个结点之前,可以另外设置一个prev指针来记录下当前的结点。

也可以采取如下的写法,直接找尾结点的前一个结点。

当链表中只有一个结点时,上述的两种写法都会导致解引用NULL的错误,所以我们需要单独处理一下这种情况。

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next->next != NULL)
		{
			ptail = ptail->next;
		}
		free(ptail->next);
		ptail->next = NULL;
	}
}

4.2 头删

释放掉头结点,然后让头结点指针指向第二个结点即可。

但是,释放掉头结点会导致我找不到第二个结点,所以我们定义了一个newphead指针来指向第二个结点。

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* newphead = (*pphead)->next;
	free(*pphead);
	*pphead = newphead;
}

4.3 删除指定位置的结点

与尾删类似,只不过要找的是某个指定的结点而不是尾结点,同样需要遍历链表。

//删除指定位置节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);

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

4.4 删除指定位置之后的结点

释放掉该位置之后的结点,然后将之后剩余的结点接到该位置之后即可。

要注意的与头删类似。

//删除指定位置之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

5. 查找结点

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
			return pcur;

		pcur = pcur->next;
	}
	return NULL;
}

6. 销毁单链表

依次删除每个结点即可。

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

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

7. 结语

单链表相对于顺序表来说,优势有:

1. 对数据进行增加删除较为方便,因为我只需要改变结点之间的链接关系即可。

2. 不会浪费空间,有需要我才会开辟新的结点(尽管每个结点都是结构体,所占空间比数组元素多)。

劣势:

1. 查找不便,只能一个一个地通过next指针来查找,且无法访问某结点的前一个结点。而顺序表则可通过下标访问操作符来对数据进行直接访问。 

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

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

相关文章

react-静态组件,动态组件

react09- 组件 静态组件 动态组件 静态组件: 函数组件,在第一次渲染完成后,组件中的内容,不会根据组件内的某些操作再次进行更新,页面并不会跟着改变 过程: 第一次渲染时,执行函数方法&#x…

202458读书笔记|《风来自你的方向》——我每次见你时的百米冲刺,加起来就是一生的长跑

《风来自你的方向》隔花人著 大绵羊BOBO绘,狗狗🐶绘本,这是看的第3本书。上俩本是《我是你的小狗 狗狗心事绘本》,《我是你的小狗2 当我有了你》。 同样的简短文字小狗🐶漫画,有爱的主人,有趣…

[中级]软考_软件设计_计算机组成与体系结构_12_概述及回顾

概述及回顾 总纲考情分析与分值海明校验码计算公式重点 总纲 考情分析与分值 海明校验码计算公式 2 r m r 1 2^r mr1 2rmr1 重点 数据的表示是计算题型的基础计算机组成中的CPU组成计算机组成中的存储系统,是核心重点的考察CISC与RISC及流水线执行时间的求取

lgwr超时如何判断存储还是cpu问题?(等待事件各种类型和说明及相关查询)

通过awr报告看: 分析: log file parallel write平均等待8毫秒 log file sync平均等待402毫秒 排查: log file sync parallel write lgwr cpu log file parallel write等待少说明存储不慢。 所以:log file sync等待长是因为…

css字体相关属性

属性汇总 属性作用font-family 设置文章字体 font-size 设置字体大小 font-weight设置字体粗细font-style设置字体斜体font总体设置以上属性 设置文章字体 font-family属性 案例: 设置字体大小 font-size属性 注意事项: 1.必须要加单位&#xff0…

很牛的一套仓库管理系统,免费复用【带源码】

今天给大家分享一套基于SpringbootVue的仓库管理系统源码,在实际项目中可以直接复用。(免费提供,文末自取) ​一、系统运行图(设计报告和接口文档) 1、登陆页面 2、物品信息管理 3、设计报告包含接口文档 二、系统搭建视频教程 …

Training - Kubeflow 的 PyTorchJob 配置 DDP 分布式训练 (ncclInternalError)

欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://blog.csdn.net/caroline_wendy/article/details/137569332 Kubeflow 的 PyTorchJob 是 Kubernetes 自定义资源,用于在 Kubernetes 上运行 PyTorch 训练任务,是 K…

谷歌浏览器快捷键, VScode 快捷键

谷歌浏览器快捷键 谷歌浏览器跳转标签页的方式: control Tab 跳转下一个标签页 control shift tab 上一个标签页 command 1-8 跳转对应的标签页,而command 9 则是跳转最后一个标签页 Previous Tab 插件实现谷歌浏览器两个tab页来回切换。快捷键为…

猫头虎分享已解决Error: 已解决“ModuleNotFoundError: No module named“

博主猫头虎的技术世界 🌟 欢迎来到猫头虎的博客 — 探索技术的无限可能! 领域矩阵: 🌐 猫头虎技术领域矩阵: 深入探索各技术领域,发现知识的交汇点。了解更多,请访问: 猫头虎技术矩阵…

Docker入门指南:从安装到基本操作和镜像构建的全面教程

文章目录 一、Docker简介二、Docker的安装三、Docker的基本概念四、Docker的基本操作五、Dockerfile和镜像构建六、总结 一、Docker简介 Docker是一个开源的应用容器引擎,它允许开发者将应用程序及其依赖项打包到一个可移植的容器中,然后在任何支持Dock…

24/04/09总结

异常: 1.异常是什么? 程序中可能出现的问题 2.异常体系的最上层父类是谁?异常分为几类? 父类:Exception。 异常分为两类:编译时异常、运行时异常 编译时异常和运行时异常的区别? 编译时异常:没有继承RuntimeException的异常,直接继承于Exception。 编译阶段就会…

阿里面试题二

实在是太长了 重新开一篇吧 dubbo 服务暴露 Dubbo——服务调用、服务暴露、服务引用过程 - 简书 这两篇文章写的是极好 我现在查得资源强的可怕朋友们 服务降级 MockClusterInvoker 负载均衡策略 容错机制在哪里实现的源码 通信 NIO、BIO区别,NIO解决了什么…

[C语言]——柔性数组

目录 一.柔性数组的特点 二.柔性数组的使用 三.柔性数组的优势 C99中,结构体中的最后⼀个元素允许是未知大小的数组,这就叫做『柔性数组』成员。 typedef struct st_type //typedef可以不写 { int i;int a[0];//柔性数组成员 }type_a; 有些编译器会…

jmeter压测websocket协议

一、jmeter 安装websocket插件 1、选项--插件管理 2、搜索WebSocket Samplers by Peter Doornbosch插件 进行安装 3、 重启 jmeter 二、jmeter压测websocket协议实战 2.1、以网站为例: websocket在线测试 1、断开连接 2、打开F12,查看WS数据 3、…

下班后开始更新进行做什么内容

今天没有完成的内容有哪些 作用插槽 没有完成 开始学习一下一个工具如何 学习一个kail 兼职挖漏洞的方式 先来安装一个windows镜像内容 安装成功了

蓝桥杯-阿坤老师的魔方挑战

图示: 代码: #include <iostream> using namespace std; int main() {int N,i,j,row,col,sum,max0;cin>>N;int ar[N][N];for(i0;i<N;i){for(j0;j<N;j){cin>>ar[i][j];}//输入矩阵 }for(i0;i<N;i){row0;coli;sum0;//重新初始化while(row<N){if(c…

基于Java+SpringBoot+vue3+uniapp口红销售/商城管理系统设计与实现

博主介绍&#xff1a;✌全网粉丝5W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

C++11:function包装器

包装器&#xff0c;体现了C11中的封装性&#xff0c;包装器可以应用于&#xff1a;函数指针&#xff0c;仿函数&#xff0c;lambda 而包装器function的出现刚好也弥补了上述三种语法的不足之处 函数指针写起来较为复杂&#xff0c;而仿函数之间类型不同&#xff0c;lambda则在…

【粉丝福利社】区块链与金融科技(文末送书-完结)

&#x1f3c6; 作者简介&#xff0c;愚公搬代码 &#x1f3c6;《头衔》&#xff1a;华为云特约编辑&#xff0c;华为云云享专家&#xff0c;华为开发者专家&#xff0c;华为产品云测专家&#xff0c;CSDN博客专家&#xff0c;CSDN商业化专家&#xff0c;阿里云专家博主&#xf…

nexus设置s3存储

问题 因为我的nexus是安装在EC2上面&#xff0c;需要利用s3的存储能力&#xff0c;为nexus提供存储服务。 步骤 准备s3桶 输入桶名&#xff0c;创建s3桶&#xff0c;如下图&#xff1a; 创建桶读写策略 具体内容如下&#xff1a; {"Version": "2012-10-1…