数据结构——单链表(Singly Linked List)

news2024/9/22 9:54:58

1.链表介绍

        链表是一种物理储存上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。

        对于上图,每一个结点都是一个结构体,这个结构体内存储的成员是值和下一个结点的地址。所以plist存储的是第一个结点的地址(即plist为一个结构体指针),而后续每个节点都会存储对于自己下一个结点的地址。由于链表的结点是通过malloc一个个申请出来的,所以其存储空间是不连续的,也是由于这个原因我们才需要通过一个结构体指针来访问逻辑上的下一个结点。

2.链表的分类

        链表具有三个特征,根据这三个特征的排列组合得知链表有8种组合。

2.1 单向和双向

        

       单向链表的每个结点成员只有指向下一个结点的指针,而没有指向上一个结点的指针。双向链表则二者皆有,所以单向链表就像“过河的卒”,不可以回头访问自己的上一个结点,而双向链表则既可以访问自己的上一个结点,也可以访问自己的下一个结点。

2.2 带头和不带头

        带头链表和不带头链表的区别就在于带头链表有一个“头结点”。

总结下来,带头链表会有一些优势:

        ① 避免空指针的访问:因为头结点又名哨兵位,所以它起到一个放哨的作用。无论链表结构是否为空,都可以放心的访问链表,因为有头结点的存在,就算链表为空也不会访问到空指针,更加安全。

        ② 函数传参不需要传递二级指针:如果没有头结点,当我对链表增删查改操作时,由于手上拿的是链表的第一个结点,所以如果操作到链表第一个位置,可能会涉及到链表第一个结点指针的变化,而传一级指针的参数是不会使该操作生效的,所以需要二级指针。但是有了头结点后无论做什么操作,头结点一旦生成直至链表销毁都不会改变,所以不需要传递二级指针,更加方便。

2.3 循环和非循环

        非循环链表的尾结点由于是最后一个结点,其后没有结点,所以它的next指针是指向NULL的。循环链表的尾结点则是指回了链表的第一个结点,使得整体结构体现出循环的功能。

        

        尽管有八种链表的组合形式,但是我们常用的的只有两种形式的链表。即单向不带头不循环链表双向带头循环链表。

3.单链表工程

对于一个单链表工程,我们一般模式需要分为三部分。

        SList.h 为头文件,其中包含库函数头文件的包含,顺序表结构体的定义与声明,接口函数的声明。

        SList.c 包含接口函数的定义。

        Test.c 是我们的测试源文件,从这里进入main函数。

 3.1 单链表的定义

        建立一个单链表需要多个结点共同构成,我们针对单链表一个结点进行定义,将其定义为一个结构体,包含数据和下一个结点的指针。

typedef int SLNDataType;

typedef struct SListNode
{
	SLNDataType val;
	struct SListNode* next;
}SLNode;

3.2 单链表的函数接口

        对一个单链表,我们需要一些函数来方便我们管理数据。因此我们所要实现的基本的函数接口需要包含增删查改等功能。

3.2.1 单链表结点插入

        单链表常用的插入数据方式有:头插,即在链表最前端插入数据结点;尾插,即在链表最后端插入数据结点;随机插入,即在指定的结点前或者后插入结点。

        单链表由于没有头结点,所以在插入时很有可能需要改变头结点,例如头插或链表为空时插入。所以在参数传递时我们不得不以二级指针的形式进行传参,这样可以在函数内部修改头结点,从而成功插入结点。

3.2.1.1 单链表创建结点

        因为在单链表的函数接口中有多个结点需要插入结点,需要频繁创建结点并以不同的方式插入,所以我们将创建节点的过程封装成一个函数,在需要创建结点时调用函数即可,避免代码冗余。

SLNode* CreateNode(SLNDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	return newnode;
}
3.2.1.2 单链表头插

        单链表头插时只需要将原来的第一个结点链接在新结点之后即可。

void SLTPushFront(SLNode** pphead, SLNDataType x)
{
	assert(pphead);

	SLNode* tmp = CreateNode(x);
	tmp->next = *pphead;
	*pphead = tmp;
}
3.2.1.3 单链表尾插

        单链表尾插需要先找到尾结点,但由于单链表的单向不循环的特性,我们只能从头遍历链表找到尾结点,才能将新结点链接在尾结点之后。

void SLTPushBack(SLNode** pphead, SLNDataType x)
{
	assert(pphead);

	if (*pphead == NULL)
	{
		*pphead = CreateNode(x);
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = CreateNode(x);
	}
}
3.2.1.4 单链表在指定结点前插入

        在指定节点前插入需要这个结点前一个结点的指针指向新结点,所以我们就需要先遍历找到这个结点的前一个结点。

//在pos节点前插入
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	SLNode* tmp = CreateNode(x);
	if (pos == *pphead)
	{
		tmp->next = *pphead;
		*pphead = tmp;
	}
	else
	{
		SLNode* prev = *pphead;
		tmp->next = pos;
		while (prev->next != pos)
		{
			//if (prev->next == NULL)
			//{
			//	printf("插入节点有误\n");
			//	exit(-1);
			//}
			prev = prev->next;
		}
		prev->next = tmp;
	}
}
3.2.1.5 单链表在指定结点后插入

        在指定节点后插入就可以不用遍历链表,直接链接即可。

void SLTInsertAfter(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	assert(pphead);
	assert(pos);

	SLNode* tmp = CreateNode(x);
	tmp->next = pos->next;
	pos->next = tmp;
}

3.2.2 单链表结点删除

        单链表常用的删除结点的方式有:头删,即删除在链表最前端的结点;尾删,即删除在链表最后端的结点;随机删除,即删除在指定的结点前或者后的结点。

3.2.2.1 单链表头删

        头删只需要将第一个结点释放掉,然后将原来指向链表第一个结点的变量赋值第二个结点即可。

void SLTPushFront(SLNode** pphead, SLNDataType x)
{
	assert(pphead);

	SLNode* tmp = CreateNode(x);
	tmp->next = *pphead;
	*pphead = tmp;
}
3.2.2.2 单链表尾删

        尾删需要遍历链表找到尾结点,然后将尾结点释放并将尾结点的前一个结点的next指针赋值为空指针。需要警惕只有一个结点的链表,需要将链表置为空。

void SLTPopBack(SLNode** pphead)
{
	assert(pphead);
	//0个节点
	assert(*pphead);
	//1个节点
	SLNode* prev = NULL;
	SLNode* tail = *pphead;
	if (tail->next == NULL)
	{
		free(tail);
		tail = NULL;
		*pphead = NULL;
	}
	else
	{
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		prev->next = NULL;
	}
}
3.2.2.3 单链表删除指定结点

        想要删除一个指定结点,需要找到其前一个结点,将前一个结点与后一个结点链接起来。

//删除pos节点
void SLTErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	
	if (pos == *pphead)
	{
		SLNode* tmp = *pphead;
		*pphead = (*pphead)->next;
		free(tmp);
		tmp = NULL;
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			/*if (prev->next == NULL)
			{
				printf("删除节点有误\n");
				exit(-1);
			}*/
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
3.2.2.4 单链表在指定结点后删除

        删除指定结点后一个结点,只需要将这个结点和它的下下个结点链接起来即可。

//在pos位置后删除
void SLTEraseAfter(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(pos->next);

	SLNode* tmp = pos->next;
	pos->next = tmp->next;
	free(tmp);
	tmp = NULL;
}

3.2.3 单链表打印

        依次遍历所有结点即可,由于不需要改变链表的值,所以可以传递一个一级指针。

void SLTPrint(SLNode* phead)
{
	while (phead != NULL)
	{
		printf("%d->", phead->val);
		phead = phead->next;
	}
	printf("NULL\n");
}

3.2.4 单链表查找

        用于查找链表中指定值的结点,会返回该结点的地址。所以,只需要遍历链表并且比较即可。

SLNode* SLTFind(SLNode* phead, SLNDataType x)
{

	SLNode* tmp = phead;
	while (tmp != NULL)
	{
		if (tmp->val == x)
		{
			return tmp;
		}
		else
		{
			tmp = tmp->next;
		}
	}
	printf("未找到节点\n");
	return NULL;
}

3.2.5 单链表销毁

        销毁单链表只需要遍历链表,依次释放所有结点即可。

void SLTDestroy(SLNode** pphead)
{
	while (*pphead != NULL)
	{
		SLNode* tmp = *pphead;
		*pphead = (*pphead)->next;
		free(tmp);
		tmp = NULL;
	}
}

4.单链表总结反思

        无头单向不循环链表,这种链表虽然结构简单但是应用广泛。在最近的学习过程中,我发现不少与链表有关的题目多用单链表来设题,除此之外,一些更加复杂的结构也需要用单链表作为支撑,所以可以看出其重要性。

        除此之外,恰恰是因为其结构简单,所以对特殊情况的限制较少,这就需要我在写代码时充分考虑可能会发生的所有情况,以便做出合理控制。毫无疑问,单链表是很重要的,我感觉它就像“原汁原味”的链表,需要反复品尝,并且一切上层建筑都是源于这看似简单的结构。

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

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

相关文章

概率论与数理统计中常见的随机变量分布律、数学期望、方差及其介绍

1 离散型随机变量 1.1 0-1分布 设随机变量X的所有可能取值为0与1两个值,其分布律为 若分布律如上所示,则称X服从以P为参数的(0-1)分布或两点分布。记作X~ B(1,p) 0-1分布的分布律利用表格法表示为: X01P1-PP 0-1分布的数学期望E(X) 0 *…

【前端】让列表像Excel单元格一样编辑

前言 领导说了一堆的话,最后总结一句就是客户很懒,客户的员工更加懒。 本着让别人节省时间的原则,提倡出了让列表和Excal的单元格一样,不仅看数据还可以随时更改数据。 查资料 根据 Jeecg-Vue3 源码介绍,从而知道是基于 Vben Admin 开源项目进行改造的。 因此在 Vben…

web前端之引入svg图片、html引入点svg文件、等比缩放、解决裁剪问题、命名空间、object标签、阿里巴巴尺量图、embed标签、iframe标签

MENU 前言直接在页面编写svg使用img标签引入通过css引入使用object标签引入其他标签参考资料 前言 web应用开发使用svg图片的方式,有如下几种方式 1、直接在页面编写svg 2、使用img标签引入 3、通过css引入 4、使用object标签引入 直接在页面编写svg 在html页面直接…

hive 报错return code 40000 from org.apache.hadoop.hive.ql.exec.MoveTask解决思路

参考学习 https://github.com/apache/hive/blob/2b57dd27ad61e552f93817ac69313066af6562d9/ql/src/java/org/apache/hadoop/hive/ql/ErrorMsg.java#L47 为啥学习error code 开发过程中遇到以下错误,大家觉得应该怎么办?从哪方面入手呢? 1.百…

【CVE-2021-1675】Spoolsv打印机服务任意DLL加载漏洞分析

漏洞详情 简介 打印机服务提供了添加打印机的接口,该接口缺乏安全性校验,导致攻击者可以伪造打印机信息,在添加新的打印机时实现加载恶意DLL。这造成的后果就是以system权限执行任意代码。 影响版本 windows_10 20h2 windows_10 21h1 win…

我在electron中集成了自己的ai大模型

同学们可以私信我加入学习群! 正文开始 前言一、大模型选择二、获取key三、调用api四、调用ai模型api时,解决跨域总结 前言 最近单位把gpt、文心一言、通义千问、星火等等等等你能想到的ai大模型都给禁掉了,简直丧心病狂。 不知道有多少感同…

【多线程】-- 01 线程创建之继承Thread多线程同步下载网络图片

多线程 1 简介 1.1 多任务、多线程 普通方法调用:只有主线程一条执行路径 多线程:多条执行路径,主线程和子线程并行交替执行 如下图所示: 1.2 程序.进程.线程 一个进程可以有多个线程,例如视频中同时听声音、看图…

unordered_map 与 unordered_set 的模拟实现

unordered_map 与 unordred_set 的模拟实现与 map 与 set 的模拟实现差不多。map 与 set 的模拟实现中,底层的数据结构是红黑树。unordered_map 与 unordered_set 的底层数据结构是哈希表。因此,在模拟实现 unordered_map 与 unordred_set 之前你必须确保…

Find My鼠标|苹果Find My技术与鼠标结合,智能防丢,全球定位

随着折叠屏、多屏幕、OLED 等新兴技术在个人计算机上的应用,产品更新换代大大加速,进一步推动了个人计算机需求的增长。根据 IDC 统计,2021 年全球 PC 市场出货量达到 3.49 亿台,同比增长 14.80%,随着个人计算机市场发…

【从浅识到熟知Linux】基本指定之zip、unzip和tar

🎈归属专栏:从浅学到熟知Linux 🚗个人主页:Jammingpro 🐟每日一句:周五写博客更刺激了,想到明天可以晚起床半小时,瞬间精神抖擞。再写它10篇博客。 文章前言:本文介绍zip…

Tars-GO 开发

默认环境是安装好的 创建服务: tarsgo make App Server Servant GoModuleName Tars 实例的名称,有三个层级,分别是 App(应用)、Server(服务)、Servant(服务者,有时也称 Object&am…

什么是AWS CodeWhisperer?

AWS CodeWhisperer https://aws.amazon.com/cn/codewhisperer/ CodeWhisperer 经过数十亿行代码的训练,可以根据您的评论和现有代码实时生成从代码片段到全函数的代码建议。 ✔ 为您量身定制的实时 AI 代码生成器 ✔ 支持热门编程语言和 IDE ✔ 针对 AWS 服务的优…

前端大厂(腾讯、字节跳动、阿里......)校招面试真题解析,让你面试轻松无压力!

前言 校招很重要,应届生的身份很珍贵!在校招的时候与我们竞争的大部分都是没有工作经验的学生,而且校招企业对学生的包容度高,一般对企业来说,社招更看重实际工作经验,而校招更愿意“培养人”,校…

Linux(7):Vim 程序编辑器

vi 基本上 vi 共分为三种模式,分别是【一般指令模式】、【编辑模式】与【指令列命令模式】。 这三种模式的作用分别是: 一般指令模式(command mode) 以 vi 打开一个文件就直接进入一般指令模式了(这是默认的模式,也简称为一般模式)。在这个模…

Rust UI开发(二):iced中如何为窗口添加icon图标

注:此文适合于对rust有一些了解的朋友 iced是一个跨平台的GUI库,用于为rust语言程序构建UI界面。 想要了解如何构建简单窗口的可以看本系列的第一篇: Rust UI开发:使用iced构建UI时,如何在界面显示中文字符 本篇是系…

量子计算的发展

目录 一、量子力学的发展历程二、量子计算的发展历程三、量子计算机的发展历程四、量子信息科学的发展 一、量子力学的发展历程 量子力学是现代物理学的一个基本分支,它的发展始于20世纪初。以下是量子力学发展的几个重要阶段: 普朗克(1900&…

ubuntu环境删除qtcreator方法

文章目录 方法1方法2方法3参考不同的安装方法,对应不同的删除方法 方法1 apt-get或者dpkg 方法2 QtCreatorUninstaller 方法3 MaintenanceTool

机器学习【02】在 Pycharm 里使用 Jupyter Notebook

只有 Pycharm 的 Professional 版才支持 Jupyter Notebook 本教程结束只能在pycharm中使用,下载的库在pycharm选中的虚拟环境中 ssh -L localhost:9999:localhost:8888 usernameip这句话每次都要用 准备 1.服务器安装jupyter sudo snap install jupyter2.在 Jup…

39 关于 binlog 日志

前言 bin log 相关 呵呵 记得之前是做过基于 binlog 的数据同步到的, 这里 可以来了解一下 binlog 的产生, 以及 相关更加详细的信息 说不定 之后也可以 做一个 binlog 的解析工具 这里 来看一下 各个常见的 binlog event 的相关格式 open-replicator 解析binlog失败 a…

npm pnpm yarn(包管理器)的安装及镜像切换

安装Node.js 要安装npm,你需要先安装Node.js。 从Node.js官方网站(https://nodejs.org)下载并安装Node.js。 根据你的需要选择相应的版本。 一路Next,直到Finish 打开CMD,输入命令来检查Node.js和npm是否成功安装 nod…