数据结构--单链表实现

                                                        欢迎光顾我的homepage

前言        

        链表和顺序表都是线性表的一种,但是顺序表在物理结构和逻辑结构上都是连续的,但链表在逻辑结构上是连续的,而在物理结构上不一定连续;来看以下图片来认识链表与顺序表的差别

这里以动态顺序表为例,和链表中的单链表对比一下

动态顺序表

单链表

        这里就可以很清晰的看到顺序表的底层其实就是一个数组,数据的是连续存储的(顺序表物理结构连续);而链表它每一个数据都不是连续的(链表物理结构上不一定连续)。

链表节点

        通过观察上图,我们会发现链表每一个节点都存放在数据和下一个节点的地址。

        那么来想一下,为了链表每一个节点都统一起来,都存储有效数据和下一个节点的地址,我们就需要创建应该结构体,来存储有效数据和下一个节点的指针;
注:这里只是单链表

typedef int SLType;
typedef struct SLTNode
{
	SLType data;
	struct SLTNode* next;
}SLT;

创建好链表节点,接下来就来实习单链表存储数据的这些功能。

单链表实现

先来看一下单链表都实现都哪些功能

//输出链表
void SLTPrint(SLT* phead);
//创建节点
SLT* SLTCreat(SLType x);
//单链表头插
void SLTPushFront(SLT** pphead, SLType x);
//单链表尾插
void SLTPushBack(SLT** pphead, SLType x);
//单链表头删
void SLTPopFront(SLT** pphead);
//单链表尾删
void SLTPopBack(SLT** pphead);
//查找数据
SLT* SLTFind(SLT* phead, SLType x);
//指定位置之前插入
void SLTInsert(SLT** pphead, SLT* pos, SLType x);
//指定位置之后插入
void SLTInsertAfter(SLT* pos, SLType x);
//删除指定节点
void SLTErase(SLT** pphead, SLT* pos);
//删除指定位置后一个节点
void SLTEraseAfter(SLT* pos);

创建节点

        这里创建节点,还是使用动态内存来创建,创建完成后,将数据存储进去,并把新节点的下一个节点置为NULL

代码如下:

//创建节点
SLT* SLTCreat(SLType x)
{
	SLT* newnode = (SLT*)malloc(sizeof(SLT));
	assert(newnode);
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

        测试一下代码是否正确

可以看到代码没有问题。

输出链表

        由于这里实现单链表,存储的是整型数据,就以整型的方式输出,若存储其他类型的数据,就以存储类型的方式输出。

输出链表,首先就要遍历链表,因为链表最后一个节点里存储的下一个节点的地址为空(即最后一个节点  ->next 为空)所以,遍历链表结束的条件就是ptail ==NULL,没输出一个就让ptail指向下一个节点,直到为空,遍历结束。

        来写代码实现:

//输出链表
void SLTPrint(SLT* phead)
{
	SLT* ptail = phead;
	while (ptail!= NULL)//也可以直接写成 ptail
	{
		printf("%d -> ", ptail->data);
		ptail = ptail->next;
	}
	printf("NULL\n");
}

这里先创建两个节点并存储数据输出看一下结果

int main()
{
	SLT* plist = SLTCreat(1);
	plist->next = SLTCreat(2);
	SLTPrint(plist);
	return 0;
}

        这里也成功输出插入的两个数据。(最后一个节点的next指向空)

单链表头插

        在链表头部插入数据,不用像顺序表那样去移动所以的有效数据,链表只需要改变一个指针的指向即可

假设现在链表中已经存在两个数据,再进行头插,这时就只需要改变plist的指向即可,当然新节点的next指针也要指向下一个节点(plist指向的节点)。

代码如下:

//单链表头插
void SLTPushFront(SLT** pphead, SLType x)
{
	assert(pphead);
	SLT* newnode = SLTCreat(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

还有一种情况,如果现在链表中没有数据,再进行头插,这里代码也能实现链表没有数据时的头插

单链表尾插

        链表的尾插,因为指针传的是指向头节点的指针的地址,所以,我们需要先遍历链表,找到链表的尾部,再修改尾节点的next指针指向。

假设现在链表中已经存在两个数据,再进行尾插,此时我们只需要找到链表的尾部,修改尾节点next指针指向即可,代码如下

//单链表尾插
void SLTPushBack(SLT** pphead, SLType x)
{
	assert(pphead);
	SLT* newnode = SLTCreat(x);
	SLT* ptail = *pphead;
	//遍历链表
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;
}

        考虑了这种情况,再来看以下链表为空的情况,如果链表为空,这里ptail->next就对空指针进行解引用操作了;所以,我们需要判断链表是否为空?如果为空,插入的新节点就是头节点,直接让plist(即*pphead)指向新节点即可;如果不为空就正常进行尾插。

最终代码如下:

//单链表尾插
void SLTPushBack(SLT** pphead, SLType x)
{	
	assert(pphead);
	SLT* newnode = SLTCreat(x);
	if (*pphead == NULL) //判断链表是否为空
	{
		*pphead = newnode;
	}
	else {
		SLT* ptail = *pphead;
		//遍历链表
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

这里代码可以正常进行尾插。

单链表头删

        链表头删,首先我们要判断链表是否为空,如果为空(空链表没有数据该如何删除呢?

链表头删,我们需要修改plist(*pphead)指向链表的下一个节点即头节点的next指针。

        此外:因为我们的节点都是动态申请的内存,所以在删除时,需要把它释放掉。

思路很简单,写一下代码:
 

//单链表头删
void SLTPopFront(SLT** pphead)
{
	assert(pphead && *pphead);
	SLT* del = (*pphead);
	*pphead = (*pphead)->next;
	free(del);
	del = NULL;
}

再来看一个如果链表为空,又是啥结果呢?

现在链表已经为空,在执行一次头删代码

这里assert断言报错。

单链表尾删

        首先尾删与头删一样,链表不能为空。

        尾删与尾插也有共同之处,就是遍历链表,找到链表的尾节点。找到尾节点,删除尾节点;然后修改尾节点上一个节点的next指针为NULL;所以在遍历链表时就要多记录一个节点。

//单链表尾删
void SLTPopBack(SLT** pphead)
{
	assert(pphead && *pphead);
	SLT* ptail = *pphead;
	SLT* pcur = *pphead;
	//遍历链表
	while (ptail->next)
	{
		pcur = ptail;
		ptail = ptail->next;
	}
	pcur->next = NULL;
	free(ptail);
	ptail  = NULL;
}

        在测试的时候我们会发现一个问题,如果链表只有一个节点,删除之后我们的plist指针并没有置为空,而我们的空间已经释放掉了,这是很危险的;所以这里我们先判断一下链表是否只有一个节点,如果是,我们释放完空间以后,将(*pphead)置为空,以免出现野指针的情况。

完善后代码:

//单链表尾删
void SLTPopBack(SLT** pphead)
{
	assert(pphead && *pphead);
	if ((*pphead)->next== NULL)
	{
		free(*pphead);
		*pphead = NULL;

	}
	else {
		SLT* ptail = *pphead;
		SLT* pcur = *pphead;
		//遍历链表
		while (ptail->next)
		{
			pcur = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		pcur->next = NULL;
	}
}

测试没有问题,代码能够完成尾删这个功能。

查找数据

        查找数据,遍历链表查找数据,如果找到就返回数据所在节点的地址,如果没有找到就返回NULL;

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

这里测试以下:

数据存在时

数据不存在时

指定节点之前插入

        在链表指定节点之前插入数据,我们还需要知道指定节点的前一个节点,所以仍然需要遍历链表,寻找指定节点pos前一个节点。

//指定位置之前插入
void SLTInsert(SLT** pphead, SLT* pos, SLType x)
{
	assert(pphead && *pphead);
	SLT* ptail = *pphead;
	if (ptail == pos)//头插
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLT* newnode = SLTCreat(x);
		while (ptail->next != pos)//找到pos位置
		{
			ptail = ptail->next;
		}
		newnode->next = pos;
		ptail->next = newnode;
	}
}

当然,这里如果故意传NULL给pos,(这就没啥意义了)这里也可以写以下assert断言pos。

指定节点之后插入

        在指定节点之后插入数据,就不需要再进行遍历链表,这里直接插入即可。

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

删除指定节点

        删除链表中的指定节点,我们需要这个节点的上一个节点,所以又需要遍历链表,找到pos上一个节点,修改pos->next指针指向。

代码如下:

//删除指定节点
void SLTErase(SLT** pphead, SLT* pos)
{
	//找到pos上一个节点
	SLT* ptail = *pphead;
	while (ptail->next != pos)
	{
		ptail = ptail->next;
	}
	SLT* p = pos->next;
	free(pos);
	pos = NULL;
	ptail->next = p;
}

删除指定节点后一个节点

        删除链表指定节点后一个节点,因为pos就是删除节点的上一个节点,所以不需要遍历链表,直接删除即可。

//删除指定位置后一个节点
void SLTEraseAfter(SLT* pos)
{
	assert(pos->next);
	SLT* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

这里如果传过来的就是链表的尾节点,那没办法删除后一个节点,所以断言pos->next;

代码总览

#include"SList.h"
//创建节点
SLT* SLTCreat(SLType x)
{
	SLT* newnode = (SLT*)malloc(sizeof(SLT));
	assert(newnode);
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
//输出链表
void SLTPrint(SLT* phead)
{
	SLT* ptail = phead;
	while (ptail != NULL)//也可以直接写成 ptail
	{
		printf("%d -> ", ptail->data);
		ptail = ptail->next;
	}
	printf("NULL\n");
}
//单链表头插
void SLTPushFront(SLT** pphead, SLType x)
{
	assert(pphead);
	SLT* newnode = SLTCreat(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
//单链表尾插
void SLTPushBack(SLT** pphead, SLType x)
{	
	assert(pphead);
	SLT* newnode = SLTCreat(x);
	if (*pphead == NULL) //判断链表是否为空
	{
		*pphead = newnode;
	}
	else {
		SLT* ptail = *pphead;
		//遍历链表
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}
//单链表头删
void SLTPopFront(SLT** pphead)
{
	assert(pphead && *pphead);
	SLT* del = (*pphead);
	*pphead = (*pphead)->next;
	free(del);
	del = NULL;
}
//单链表尾删
void SLTPopBack(SLT** pphead)
{
	assert(pphead && *pphead);
	if ((*pphead)->next== NULL)
	{
		free(*pphead);
		*pphead = NULL;

	}
	else {
		SLT* ptail = *pphead;
		SLT* pcur = *pphead;
		//遍历链表
		while (ptail->next)
		{
			pcur = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		pcur->next = NULL;
	}
}
//查找数据
SLT* SLTFind(SLT* phead, SLType x)
{
	SLT* ptail = phead;
	while (ptail)
	{
		if (ptail->data == x)
		{
			return ptail;
		}
		ptail = ptail->next;
	}
	return NULL;
}
//指定位置之前插入
void SLTInsert(SLT** pphead, SLT* pos, SLType x)
{
	assert(pphead && *pphead);
	SLT* ptail = *pphead;
	if (ptail == pos)//头插
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLT* newnode = SLTCreat(x);
		while (ptail->next != pos)//找到pos位置
		{
			ptail = ptail->next;
		}
		newnode->next = pos;
		ptail->next = newnode;
	}
}
//指定位置之后插入
void SLTInsertAfter(SLT* pos, SLType x)
{
	assert(pos);
	SLT* newnode = SLTCreat(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
//删除指定节点
void SLTErase(SLT** pphead, SLT* pos)
{
	//找到pos上一个节点
	SLT* ptail = *pphead;
	while (ptail->next != pos)
	{
		ptail = ptail->next;
	}
	SLT* p = pos->next;
	free(pos);
	pos = NULL;
	ptail->next = p;
}
//删除指定位置后一个节点
void SLTEraseAfter(SLT* pos)
{
	assert(pos->next);
	SLT* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

感谢各位大佬支持并指出问题,

如果本篇内容对你有帮助,可以一键三连支持以下,感谢支持!!!

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

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

相关文章

实现沉浸式体验的秘诀:深入了解折幕投影技术!

在当今多媒体技术的浪潮中,投影技术已蜕变成为超越传统内容展示范畴的非凡工具,它深度融合了互动性与沉浸感,成为连接观众与虚拟世界的桥梁。折幕投影技术,作为这一领域的璀璨明珠,更是以其独特而神奇的手法&#xff0…

小酌消烦暑|人间正清欢

小暑是二十四节气之第十一个节气。暑,是炎热的意思,小暑为小热,还不十分热。小暑虽不是一年中最炎热的时节,但紧接着就是一年中最热的节气大暑,民间有"小暑大暑,上蒸下煮"之说。中国多地自小暑起…

开发必备基础知识【字符编码合集】

开发必备基础知识【字符编码合集】 大家在日常开发交流中会发现,别人那里运行的好好的文件,在你电脑上却无法编译,甚至出现一堆莫名其妙的字符,比如:��� 程序中经常遇到一些关于乱码…

科普文:如何进行有效沟通

概叙 你会沟通吗? 你知道正确的沟通应该怎么做吗? 在日常生活和工作中,不会沟通带来的困扰是否让你感同身受? 在工作中,你是否因表达不清让观点无法被同事理解和采纳,影响职业发展? 与上级交流是…

开源全新H5充值系统源码/自定义首页+充值页面/灵活对接上游渠道接口

开源全新H5充值系统源码,系统基于thinkphp框架开发,功能已全完善,可灵活对接其他上游渠道接口,默认对接了大猿人接口,另外可无限制自定义创建充值页面,首页支持后台自定义修改,支持三级分销&…

STM32嵌入式工业机器人控制系统教程

目录 引言环境准备工业机器人控制系统基础代码实现:实现工业机器人控制系统 4.1 数据采集模块 4.2 数据处理与分析 4.3 运动控制系统实现 4.4 用户界面与数据可视化应用场景:工业自动化与优化问题解决方案与优化收尾与总结 1. 引言 工业机器人控制系统…

Java基础(六)——继承

个人简介 👀个人主页: 前端杂货铺 ⚡开源项目: rich-vue3 (基于 Vue3 TS Pinia Element Plus Spring全家桶 MySQL) 🙋‍♂️学习方向: 主攻前端方向,正逐渐往全干发展 &#x1…

计算机应用数学--第二次作业

第二次作业计算题编程题 第二次作业 计算题 给定图 G G G(如图 1,图中数值为边权值),图切割将其分割成多个互不连通的⼦图。请使⽤谱聚类算法将图 G G G 聚类成 k 2 k 2 k2 类,使得: (a) RatioCut 最…

《向量数据库指南》——Milvus Cloud索引增强如何提升 RAG Pipeline 效果?

索引增强 1.自动合并块 在建立索引时,分两个粒度搭建,一个是chunk本身,另一个是chunk所在的parent chunk。先搜索更细粒度的chunks,接着采用一种合并的策略——如果前k个子chunk中超过n个chunk属于同一个parent chunk&#xff0c…

架构师学习理解和总结

1.架构设计理念 2.架构方法论 2.1需求分析 2.1.1常见需求层次 2.1.2 常见需求结果 2.1.3 需求与架构关系 2.2 领域分析 2.3 关键需求 2.4 概念架构设计 2.5 细化架构设计 2.6 架构设计验证 3.架构设计工具 3.1 DDD领域建模 3.2 41视图分析法 3.3 UML设计工具 4.架构师知…

Pathformer: multi-scale transformer

文章目录 摘要1 引言贡献 2 相关工作时间序列预测时间序列的多尺度建模 3 方法论3.1 多尺度变压器块多尺度划分双重注意力 3.2 自适应路径多尺度路由器多尺度聚合器 摘要 用于时间序列预测的变压器模型主要针对有限或固定尺度的时间序列进行建模,这使得捕捉跨越不同…

竞赛选题 卷积神经网络手写字符识别 - 深度学习

文章目录 0 前言1 简介2 LeNet-5 模型的介绍2.1 结构解析2.2 C1层2.3 S2层S2层和C3层连接 2.4 F6与C5层 3 写数字识别算法模型的构建3.1 输入层设计3.2 激活函数的选取3.3 卷积层设计3.4 降采样层3.5 输出层设计 4 网络模型的总体结构5 部分实现代码6 在线手写识别7 最后 0 前言…

如何实现一套键盘鼠标控制两台计算机(Mouse Without Borders快速上手教程)

需求背景 当我们需要同时使用一台主机和一台笔记本的时候,如果使用两套键盘和鼠标分别操作各自的系统,非常地不便捷且非常占据桌面空间。那么如何使用一套键盘鼠标控制两台电脑呢? 需求实现 软件说明 我们可以使用微软官方的一款软件Mous…

nodejs 获取客服端ip,以及获取ip一直都是127.0.0.1的问题

一、问题描述 在做登录日志的时候想要获取客户端的ip, 网上查了一下 通过 req.headers[x-forwarded-for] || req.connection.remoteAddress; 获取, 结果获取了之后不管是开发环境,还是生产环境获取到的一直都是 127.0.0.1,这是因为在配置N…

代码随想录算法训练营第22天|LeetCode 77. 组合、216.组合总和III、17.电话号码的字母组合

1. LeetCode 77. 组合 题目链接:https://leetcode.cn/problems/combinations/description/ 文章链接:https://programmercarl.com/0077.组合.html 视频链接:https://www.bilibili.com/video/BV1ti4y1L7cv 思路:利用递归回溯的方式…

开启视频创作新篇章!腾讯发布MimicMotion:单张图像+简单姿势,瞬间“活”化视频。

腾讯和上交发布了一个根据图片生成跳舞视频的项目MimicMotion。效果同时支持面部特征和唇形同步,不止可以搞跳舞视频,也可以做数字人。 MimicMotion方案优化的内容有: 引入基于置信度的姿态引导机制。确保生成的视频在时间上更加连贯流畅。 …

计算机图形学入门25:BRDF的测量

1.前言 BRDF(双向反射分布函数)可以用各种各样的材质去描述,但是这只是一种基于物理的描述或者近似,那什么是真正的BRDF?只有测出来的才是真正的。 为什么要测出BRDF?因为之前所描述的BRDF并不准确。如下图所示,以菲涅…

C++——模板详解(下篇)

一、非类型模板参数 模板参数分为类型形参与非类型形参。 类型形参即:出现在模板参数列表中,跟在class或者typename之后的参数类型名称。 非类型形参,就是用一个常量作为类(函数)模板的一个参数,在类&#…

LabVIEW与OpenCV图像处理对比

LabVIEW和OpenCV在图像处理方面各有特点。LabVIEW擅长图形化编程、实时处理和硬件集成,而OpenCV则提供丰富的算法和多语言支持。通过DLL、Python节点等方式,OpenCV的功能可在LabVIEW中实现。本文将结合具体案例详细分析两者的特点及实现方法。 LabVIEW与…

解决Docker Desktop启动异常 Docker Desktop- WSL distro terminated abruptly

异常 当打开Docker Desktop时候,启动docker引擎时,提示 加粗样式文本信息 Docker Desktop - WSL distro terminated abruptly A WSL distro Docker Desktop relies on has exited unexpectedly. This usually happensas a result of an external entit…