无头单向非循环链表的实现

1.链表的结构

在用代码实现之前,我们要先了解这种链表的逻辑结构和物理结构,

在逻辑上我们能知道这种链表能够通过前一个节点的 next 来存储下一个节点的地址,从而能够找到下一个数据,这就是一种线性结构,我们可以把数据看成是按照一定的顺序存储起来的,虽然他们的空间顺序不一定是他们的逻辑顺序。上面图中的 head 并不是指的链表是有头链表,head不是烧饼位,而是链表头节点,存储第一个数据的空间。我们使用链表时只要知道头节点的位置,就能访问到所有的数据,如果链表为空,则一个节点也没有。

这就是链表的物理结构,通过对节点的 next 中存的地址解引用就能访问到下一个节点,而尾节点的 next 我们设置为空指针。

2.链表的实现

链表的物理结构和逻辑结构我们清楚之后,我们就能找到每个节点的情况,无非就是数据域和指针域,数据域用来存储数据,而指针域用来存储下一个节点的指针,对于每一个节点我们用结构体定义出来就是这样的:

typedef int SLTDataType;

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

而对于后续的链表的操作,我们只需要传头结点的地址就够了,因为链表通过头节点就能访问到所有数据。而链表没有数据时,没有头节点,链表地址就是空指针。所以我们一般首先将plist初始化为NULL,而因为链表没有元素时什么也没有,所以现在要实现的这个单链表是不需要初始化的。

void test1()
{
	SLTNode* plist = NULL;
	//对plist进行操作

}

下面就是要实现的一些接口

头插

对单链表头插数据十分简单。我们首先要创建一个newnode,因为创建节点的功能我们在后面的各种插入数据中都要用到,所以我们直接写一个函数来完成创建节点的功能

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

我们在函数中创建一个新节点,将新节点的的数据初始化为插入的数据,next设置为NULL。然后返回新节点地址。

然后头插数据我们具体要怎么做呢?

看图就很简单了,我们只需要 phead 指向新节点,然后原来的链表链接到新节点的next就完成了。

但是头插是要改变 phead 的,也就是整个链表的头节点地址 plist ,所以在头插函数中我们要传plist 的地址,也就是一个二级指针,记住,要修改什么类型的变量就要传那个类型的指针,plist是SLTNode*类型的指针变量,要修改 plist 就要传plist的地址,在函数参数部分就要用SLTNode** 的指针来接收。

//头插数据
void SLTPushFront(SLTNode** pphead, SLTDataType x);

传参的时候,可能链表为空,也就是phead(plist)为NULL,但是 plist 是一个变量,他的地址不可能为空,也就是pphead 不能为NULL,如果为空,就说明传参有问题,我们可以用assert断言一下,后续的很多结构都要对一些不可能为空的指针断言检查。链表为空和链表不为空时的操作其实是一样的逻辑和代码,因为phead为NULL的时候,将 phead 连接到newnode的时候,不会对newnode有什么影响,newnode的next还是NULL,也就是它既是头指针也是尾指针。

//头插数据
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	SLTNode* newnode = NewSLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

为了方便测试,我们先写一个打印链表的函数出来。

打印链表

打印函数我们只需要从头结点遍历到NULL就行了。因为打印链表不会出现需要修改头结点的请跨,所以我们传 plist 就够了,同时因为链表可能为空,我们不用对传过来的phead断言。

//打印链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)//当cur为空时就结束
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	//为了更加直观,在最后面打印一个NULL
	printf("NULL\n");

}

写完打印函数之后先来测试一下头插的函数

	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 5);
	SLTPrint(plist);

头插函数没有问题。

头删

头删函数我们一样看图来分析

我们只需要把phead指向的节点删除,在这之前要修改phead指向第二个元素,要注意先换phead的指向再去free第一个节点,否则就出现野指针的问题了。同时,我们要注意头节点要修改,我们要传二级指针pphead,而且,删除元素的时候链表不能是空链表,所以我们还要对 *pphead也进行断言。

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	//先记录要删除的节点
	SLTNode* del = *pphead;
	//调整头结点指针
	*pphead = (*pphead)->next;
	//最后释放原来的头节点
	free(del);
}

然后对函数进行测试,我们一个一个把数据删完

	//头插
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);
	
	//头删
	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

测试链表为空再删除一下的情况

assert断言失败报错,说明函数功能是没问题的。因为当链表只有一个节点时,我们先将 phead 的值修改为phead的next,也就是NULL,所以删完之后phead又重新变成了空指针。

尾插

单链表的尾插是一件很麻烦的事情,因为他首先要找到尾节点,而由于他是单向非循环的,所以找尾节点只能遍历。我们用一个 cur 的指针来遍历链表

而cur什么时候停下来呢,尾节点有一个特点,就是他的next为NULL,所以我们的遍历的结束条件就是cur->next!=NULL。但是光这样还不行,尾插的对象可能是一个空链表,也就是cur为NULL,这时候对cur解引用就会出错。所以尾插也要检查链表是否为空,如果为空,也要修改头结点的指针,所以尾插传的也是 pphead (&plist)。而当链表为空时我们可以复用头插的函数来插入数据 。

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	if (*pphead == NULL)//如果为空链表
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = NewSLTNode(x);
		SLTNode* cur = *pphead;
		while (cur->next)
		{
			cur = cur->next;
		}
		//找到尾节点将newnode连接到尾节点的next
		cur->next = newnode;
	}
}

然后对尾插进行测试


void test2()
{
	SLTNode* plist = NULL;
	//对plist进行操作
	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
}

尾删

尾删的第一步和尾插一样都是找尾节点,但是又有一点不一样,就是尾删需要用一个指针prev来保存尾节点的前一个节点,因为我们不仅要释放掉最后一个节点,还要对倒数第二个节点的next置为NULL。当然尾删的前提是链表不为空,可以用assert断言,同时,因为删除数据可能会把链表删为空,也就是可能会改变phead,所以也要传二级指针。这里我们要思考一下要不要将只有一个节点的请跨单独拿出来讨论。

当只有一个节点时,cur->next为NULL,所以不会进入循环中,这时候我们就不需要定义一个prev指针了,而是直接释放掉phead,然后要通过二级指针把plist置为NULL。

有了思路代码就很好写了

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLTNode* cur = *pphead;
	if (cur->next == NULL)//只有一个节点的情况
	{
		free(cur);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev=NULL;
		while (cur->next)
		{
			prev = cur;
			cur = cur->next;
		}//循环退出时prev为倒数第二个节点
		prev->next = NULL;
		free(cur);
	}
}

首先我们来测试正常情况下的尾删


void test2()
{
	SLTNode* plist = NULL;
	//对plist进行操作
	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
	SLTPopBack(&plist);
	SLTPrint(plist);

}

当链表为空时再次尾删

数据查找

查找函数也没什么可取巧的,我们就是要遍历链表,一一比对数据,如果链表中能找到该数据,则返回节点的指针,如果找不到就返回空指针。

//查找数据,返回节点地址
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)//遍历查找
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	//遍历完找不到就返回NULL
	return NULL;
}

而查找函数返回了节点的指针之后,我们就不用再额外写一个修改函数了,因为我们可以直接对pos的data进行操作。

测试一下查找和修改

	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);
	
	SLTNode* pos = SLTFind(plist, 1);
	if (pos != NULL)
	{
		pos->data = 30;
		SLTPrint(plist);
	}
	else
	{
		printf("找不到\n");
	}

指定位置插入

链表指定位置插入的逻辑是在该位置之前插入,这时候我们就需要遍历查找pos的前一个位置。在这个函数中我们不再讨论 pos 是否为链表中的节点位置,只讨论Find函数的返回值中NULL和节点指针两个方面,如果pos传的是NULL,就报错,这就要求使用者在函数传参使得要注意一点了。同时,因为传的pos有可能是头结点的地址,如果是头节点的地址,我们就用头插的做法来写。

//指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	SLTNode* newnode = NewSLTNode(x);
	if (pos == *pphead)//头结点的情况
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)//我们不讨论pos是非节点指针
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

测试插入,首先测试尾节点

	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);
	
	SLTNode* pos = SLTFind(plist, 1);
	SLTInsert(&plist, pos, 30);
	SLTPrint(plist);
	

再测试头节点

指定位置之前插入数据的效率有点低,要遍历一遍,但是我们发现它在指定位置之后插入就很方便,于是我们再来实现一个指定位置之后插入的函数

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

测试首尾节点

	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);
	
	SLTNode* pos = SLTFind(plist, 4);
	SLTInsertAfter(&plist, pos, 30);
	SLTPrint(plist);
	pos = SLTFind(plist, 1);
	SLTInsertAfter(&plist, pos, 10);
	SLTPrint(plist);
	

指定位置删除

指定位置删除的逻辑与指定位置插入差不多,也是要先找到pos的前一个结点,然后将pos的前一个结点的next修改为pos的next。由于pos也与可能是头节点,所以有可能会修改头节点,要传二级指针。

//指定位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (*pphead == pos)//头删
	{
		*pphead = pos->next;
		free(pos);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
			free(pos);
	}
}

测试头尾的删除

	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);
	
	//SLTNode* pos = SLTFind(plist, 4);
	//SLTInsertAfter(&plist, pos, 30);
	//SLTPrint(plist);
	//pos = SLTFind(plist, 1);
	//SLTInsertAfter(&plist, pos, 10);
	//SLTPrint(plist);
	//

	SLTNode* pos = SLTFind(plist, 4);
	SLTErase(&plist, pos);
	SLTPrint(plist);
	pos = SLTFind(plist, 1);
	SLTErase(&plist, pos);
	SLTPrint(plist);

其实这个函数还有一点点小问题,那就是对pos释放之后没有对函数外面的pos置空,如果要置空的话,就要传pos的指针,又是一个二级指针,但是在我们看来,这是使用者应该做的事。

总结

以上就是单链表的实现,它的优势就是头插头删很方便,效率很高,但是单链表除了头部操作,其他的操作都很不适合,想要做到任意位置的高效的插入删除还是要看后面要学的带头双向循环链表

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

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

相关文章

智能感应门改造工程

今天记录一下物联网专业学的工程步骤及实施过程 智能感应门改造工程 1 规划设计1.1 项目设备清单1.2项目接线图 软件设计信号流 设备安装与调试工程函数 验收 1 规划设计 1.1 项目设备清单 1.2项目接线图 软件设计 信号流 设备安装与调试 工程函数 工程界面: using System; …

循环队列的实现及应用——桶排序bucket_sort、基数排序radix_sort

一、循环队列的实现 代码解释 1、完成初始化 2、定义方法 3、测试实例 4、完整代码 class AQueue:def __init__(self, size=10):self.__mSize = sizeself.__front=0self.__rear = 0self.__listArray = [None] * size#清空元素def clear(self):self.__front = 0self.__rear =…

腾讯游戏革命:手游内400+AI角色个性化成长,成本削减90%|TodayAI

在全球游戏开发者大会(GDC)上,腾讯游戏以一场技术革新的展示,震撼了整个游戏界。最令人瞩目的是,《火影忍者》手游中包含了超过400个AI角色,每个角色都拥有独特的个性。这一成就得益于腾讯新开发的大规模强…

江协科技STM32:TIM输出比较

输出比较模块的主要功能:输出一定频率和占空比的PWM波形 CC是捕获比较的意思,R是Register,寄存器的意思,CCR捕获比较寄存器它是输入捕获和输出比较共用的 当使用输入捕获,它就是捕获寄存器 当使用输出比较,它就是比…

RK3588 NPU 研究(二)

RK提供了两个模型,mobilenet和YOLO5。 mobilenet模型相对小,使用起来不是很明显yolo5模型大一些,可以对88种目标进行检测,提供检测的结果包括类别、包围框坐标、可信度等信息。基于rknn_yolov5_demo进行分析。 rknn_yolov5_demo基…

Vue3全家桶和小兔鲜儿案例

查看node.js版本,需要是16.0以上版本 node -v创建一个vue应用 npm init vuelatest在windows窗口中进入vs code命令 code ./创建项目后vs code打开安装依赖 npm install安装好以后运行程序 打开页面 deep有性能损耗,尽量不开启deep 生命周期函数指…

记录Linux系统中vim同时开多个窗口编辑文件

在使用Linux进行文本编辑的时候,通常使用vim编辑器编辑文件,当然啦,vim也可以创建文件,如果只是一个一个创建,只需要vim创建即可,但是如何一次性打开多个窗口编辑呢? 目录 1、目标:…

Unity和Android的交互

Unity和Android的交互 一、前言二、Android导出jar/aar包到Unity2.1 版本说明2.2 拷贝Unity的classes.jar给Android工程2.2.1 classes.jar的位置2.2.2 Android Studio创建module2.2.3 拷贝classes.jar 到 Android工程并启用 2.3 编写Android工程代码2.3.1 创建 MainActivity2.…

springboot之mybatisPlus多表查询及分页查询

文章目录 一、多表查询二、mybatis-plus条件查询三、分页查询 一、多表查询 可能会用到的注解 这里的场景是,查询每个用户及其所有的订单。就是查询你的id号的同时,把你所有的历史订单信息都拉出来。 表结构这样 CREATE TABLE User ( id INT PRIMARY…

docker笔记(一):安装、常用命令

一、docker概述 1.1docker为什么会出现 各种环境配置十分繁琐,每一个机器都需要配置环境,难免出现各种问题。 发布一个项目jar需要配置(MySQL、redis、jdk、…),项目不能都带上环境安装打包: 传统&…

PostgrerSQL基本使用与数据备份

前言 上篇了解了 PostgrerSQL 数据库的部署PostgreSQL关系型数据库介绍与部署-CSDN博客,本篇将继续就其基本操作、备份与还原内容做相关介绍。 目录 一、数据库的操作 1. 本机登录 2. 开启远程登录 2.1 开放远程端口 2.2 编辑配置文件 2.3 修改配置密码 2.…

前端三剑客 —— JavaScript (第一天)

目录 回顾内容 1.弹性布局 2.网格布局 JavaScript 概述 发展 浏览器 什么是Javascript JavaScript 能干什么 JavaScript需要的环境 JavaScript初体验 基本数据 JS书写方式 行内JS 页面JS 外部JS 1)创建外部JS文件 2)编写页面 对话框 警…

【踩坑日记】因不同系统换行符不同导致的文本读取结果不同的问题

文章目录 1 问题现象描述2 解决过程(点击直接跳到解决方法)3 原因解释4 如何避免踩坑4.1 格式转换4.2 格式查看 1 问题现象描述 起因是群友问了这么一个问题 确实很奇怪,按理说第二个printf不会完全不输出,于是想到,…

C++数据结构与算法——回溯算法组合问题

C第二阶段——数据结构和算法,之前学过一点点数据结构,当时是基于Python来学习的,现在基于C查漏补缺,尤其是树的部分。这一部分计划一个月,主要利用代码随想录来学习,刷题使用力扣网站,不定时更…

SD-WAN如何解决更有性价比地跨境网络问题

云桥通SD-WAN利用智能路由和负载均衡技术,优化数据传输路径,提高网络性能和可靠性。这意味着数据在跨国传输时可以更快到达目的地,减少延迟和丢包率。跨境SD-WAN提高了网络连接速度和质量,使用户能够更快地访问跨国业务所需的资源…

索引的概念

索引的概念    1.索引是一种可选的与表相关的数据库对象,用于提高数据的查询效率。    2.索引是一种有序的数据结构。    3.如果一个表没有创建索引,则对该表进行查询时需要进行全表扫描;如果创建了索引,则在有条件查询时…

应用性能分析工具CPU Profiler

简介 本文档介绍应用性能分析工具CPU Profiler的使用方法,该工具为开发者提供性能采样分析手段,可在不插桩情况下获取调用栈上各层函数的执行时间,并展示在时间轴上。 开发者可通过该工具查看TS/JS代码及NAPI代码执行过程中的时序及耗时情况…

福州装修答疑 | 飘窗能不能砸掉?福州中宅装饰,福州装修

装修中的飘窗是一种常见的装饰元素,它不仅可以增加室内的采光和通风效果,还能为居室增添一份雅致和温馨。然而,很多业主在装修中都会遇到一个共同的问题:装修中的飘窗到底能不能砸?什么情况下可以砸?什么情…

IO流【带有缓冲区的字节输入、输出流;字符输入、输出流 转换流】

day35 学习注意事项 按照流的发展历史去学习注意流与流之间的继承关系举一反三 IO流 继day36 字节流继承图 字节流 应用场景:操作二进制数据(音频、视频、图片) abstract class InputStream – 字节输入流的基类(抽象类&#xff0…

基于R、Python的Copula变量相关性分析及AI大模型应用

在工程、水文和金融等各学科的研究中,总是会遇到很多变量,研究这些相互纠缠的变量间的相关关系是各学科的研究的重点。虽然皮尔逊相关、秩相关等相关系数提供了变量间相关关系的粗略结果,但这些系数都存在着无法克服的困难。例如,…