详解单链表(内有精美图示哦)

news2024/11/26 18:30:51

全文目录

  • 引言
  • 链表
    • 链表的定义与结构
    • 链表的分类
  • 单链表的实现及对数据的操作
    • 单链表的创建与销毁
      • 创建
      • 销毁
    • 单链表的打印
    • 单链表的头插与头删
      • 头插
      • 头删
    • 单链表的尾插与尾删
      • 尾插
      • 尾删
    • 单链表的查找
    • 单链表在pos位置后插入/删除
      • 插入
      • 删除
    • 单链表在pos位置插入/删除
      • 插入
      • 删除
  • 总结

引言

在上一篇文章中,我们了解了顺序表的相关知识,并且实现了用顺序表管理数据。

但在这过程中,我们发现了使用顺序表管理数据时,其实是存在一些不方便的:
比如当存储空间已经被扩容的时候,删除了许多的数据,就会导致大片的内存浪费;
比如当我们需要扩容时,可能会异地扩容,这个过程会比较影响效率;
再比如当我们需要在顺序表前面插入数据时,过程会比较麻烦。

相对于顺序表,同属线性表的链表在这些方面就有着比较好的表现。

链表也有许多不同的种类:单向或双向链表、带头或不带头的链表、循环或非循环的链表等。在接下来的几篇篇文章中就会详细介绍链表的相关知识:

链表

链表的定义与结构

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

也就是说,链表是逻辑上连续,但存储结构上不连续的线性结构。我们可以通过指针访问到链表中的下一个元素。所以,在一个链表的结点中,应该至少包含两个元素:当前结点的数据与指向下一个结点的指针。

这就需要我们用到结构体的知识:我们在学习结构体时介绍过结构体的自引用,即结构体中的一个成员类型是结构体指针。即,我们可以将链表结点的类型定义为(当然,这是最简单的形式,只能依次访问链表的元素):

typedef int SLTDateType;

typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

链表的分类

链表有许多的类型:带头与不带头、循环与非循环、单向与双向:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当然,这些种类之间有许多的组合方式,根据需要,可以定义各种各样的链表。

其中,最为简单的就是无头单向非循环链表,这也是此篇文章介绍的重点:

单链表的实现及对数据的操作

对于单链表结点的结构,与上面我们介绍的栗子相同,就是最为简单的类型:
包括当前结点的数据与指向下一个节点的结构体指针:

typedef int SLTDateType;

typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

与学习顺序表时类似,我们可以实现一下用单链表来管理数据。同样的,这些功能的实现都将封装为函数:

在这之前,我们首先需要定义一个结构体指针plist,用于访问单链表的第一个结点。这个指针被初始化为NULL:

SListNode* plist = NULL;

单链表的创建与销毁

创建

在开辟顺序表的空间时,我们可以直接动态申请一块连续的空间来存放一些数据。但是对于单链表而言,它在物理空间上不是连续的。所以当我们为单链表开辟空间时,就需要一个结点一个结点分别开辟空间。

首先我们需要一块大小为结构体大小的空间,这块空间可以动态开辟:

SListNode* plist = (SListNode*)malloc(sizeof(SListNode));

在开辟某一个空间后,我们需要将这块空间初始化:将该结点的data成员初始化为想要存储的数据(这个数据可以作为参数传给函数),然后将这个结点的next成员初始化为NULL。

最后,返回已经成功创建的结点的结构体指针:

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
	SListNode* plist = (SListNode*)malloc(sizeof(SListNode));
	if (plist == NULL)
	{
		perror("malloc");
		return NULL;
	}
	plist->data = x;
	plist->next = NULL;
	return plist;
}

单链表的结点是动态开辟的,当然需要将这些空间依次释放掉,以免出现内存泄漏的问题。

销毁

当依次销毁单链表的每一个节点时,我们需要两个指针变量cur与aftercur。通过这两个指针变量,aftercur可以在cur被销毁前记录cur->next的值,从而实现在销毁cur指向的空间后,可以通过aftercur访问到下一个空间而继续进行销毁操作。
依次循环,当cur为NULL时终止,实现销毁每一个结点:
在这里插入图片描述

// 单链表的销毁
void SListDestroy(SListNode* plist)
{
	SListNode* cur = plist;
	SListNode* aftercur = cur->next;
	while (cur)
	{
		aftercur = cur->next;
		free(cur);
		cur = aftercur;
	}
}

单链表的打印

打印单链表时, 我们只需要遍历单链表,并逐个打印每个结点的data成员即可。

需要注意的是,我们在遍历时,条件必须为cur,而不是cur->next。因为当cur->next为NULL时,cur是最后一个结点。此时,最后一个结点不进入循环,该结点的数据也不会被打印:

// 单链表打印
void SListPrint(SListNode* plist)
{
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
}

单链表的头插与头删

头插

在顺序表中,想要从顺序表的前面插入数据是比较麻烦的,这需要将顺序表中的元素整体向后移动一个元素,而获得存放新与元素的空间;

但是在单链表中,头插的实现就比较简单,只需要将新创建的结点接入到单链表的前面即可。我们可以通过让新结点的next成员指向原plist,plist的值指向新结点的方式来实现:
在这里插入图片描述
需要注意的是:
在头插时,是需要改变结构体指针plist的值的,所以我们在传入该结构体指针时,需要传该结构体指针的地址,即二级指针。这样,才能实现将plist的值真正的改变:

当然,当单链表中没有元素时,即plist为NULL时,只需要改变plist的值即可。

void SListPushFront(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		newnode->next = *pplist;
		*pplist = newnode;
	}
}

头删

在顺序表中实现从前面删除也比较麻烦,需要将顺序表中的元素整体向前移动一个数据;

而单链表中只需要使plist指针指向链表中第一个结点的next成员即可。我们可以通过用一个结构体指针cur来记录plist的值,当plist指向下一个结点后,再释放cur指向的空间,即原第一个结点的空间:
在这里插入图片描述

同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。

当然,当plist为空指针时,当然是不能再删除的,所以我们可以assert断言一下。

void SListPopFront(SListNode** pplist)
{
	assert(*pplist);
	SListNode* cur = *pplist;
	*pplist = cur->next;
	free(cur);
	cur = NULL;
}

单链表的尾插与尾删

尾插

单链表需要在末尾插入数据时,首先需要找到单链表末尾的位置,然后将单链表最后一个结点的next成员改为新结点的地址即可。
在这里插入图片描述
在找最后一个元素时,我们可以通过cur指针向后移动,直到cur指向的结构体的next成员为NULL时,即cur指向的结点就是单链表的最后一个结点:
在这里插入图片描述
当然,当单链表中没有元素时,即plist为NULL时,只需要改变plist的值即可。
同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。

// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		SListNode* cur = *pplist;
		while (cur->next)
		{
			cur = cur->next;
		}
		cur->next = newnode;
	}
}

尾删

单链表删除末尾的数据时,同样的,我们需要找到单链表末尾的位置。

然后将单链表中倒数第二个结点的next成员改为NULL,然后释放cur(最后一个结点的指针)指向的空间。
我们可以通过创建一个beforecur变量来存储cur前一个结点的地址,这样,就可以实现当cur指向最后一个元素时,beforecur为倒数第二个元素:
在这里插入图片描述
当单链表中只有一个元素时,cur->next的值本身就是NULL。此时只需要释放cur指向的空间(第一个结点),然后将plist的值改为NULL即可。

同样的,由于我们需要改变结构体指针plist的值,就需要传plist的地址,即二级指针。
当然,当plist为空指针时,当然是不能再删除的,所以我们可以assert断言一下。

// 单链表的尾删
void SListPopBack(SListNode** pplist)
{
	assert(*pplist);
	SListNode* cur = *pplist;
	SListNode* beforecur = NULL;
	while (cur->next)
	{
		beforecur = cur;
		cur = cur->next;
	}
	if (beforecur==NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		free(beforecur->next);
		beforecur->next = NULL;
	}
}

单链表的查找

之后,我们就会想到要删除单链表中指定的结点。
在删除指定的结点之前,我们首先需要实现一个算法,通过结点中的data成员找到这个节点的位置。并返回这个节点的指针。

遍历单链表,只需要将结构体指针cur依次后移即可。当cur->data的值为x时,返回cur。

// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

单链表在pos位置后插入/删除

在获取到了pos后,我们就会想要实现在pos位置进行插入或删除:

插入

在插入时,我们很容易想到将新节点newnode->next的值改为pos->next,然后将pos->next的值改为newnode。从而实现将pos位置插入数据:
在这里插入图片描述
显然,这样的算法只能实现在pos后增加结点,想要在pos位置增加结点,这样的条件显然是不足的。

所以我们就先来实现一下在pos后增加数据:

// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

删除

在实现删除pos位置的结点时,我们会想到将pos->next的值改为pos->next->next位置的值,然后再释放pos后面的一块空间。
我们可以使用一个afterpos指针来暂存pos->next的值,以方便释放空间:
在这里插入图片描述
同样的,我们发现,这样删除只能释放pos后的一块空间。想要删除pos位置的结点,这样的条件显然是不足的。

但我们可以先实现一下这个函数:

// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{
	SListNode* afterpos = pos->next;
	pos->next = afterpos->next;
	free(afterpos);
	afterpos = NULL;
}

单链表在pos位置插入/删除

要想在pos位置插入结点,或删除pos位置的结点,我们需要获取到pos结点前面的结点的指针,然后改变pos前面结点中的next成员,由此实现对pos位置数据的操作。

所以在传参的时候,我们需要将单链表第一个结点的地址传给函数,将第一个结点的地址向后遍历得到pos前一个结点的地址后,再进行操作。

我们可以定义一个beforepos指针:

在这里插入图片描述

插入

在获取到beforepos后,我们就可以重复上面的操作来实现在pos位置添加一个新结点:将新节点newnode->next的值改为beforepos->next,然后将beforepos->next的值改为newnode。从而实现在pos位置插入数据:
在这里插入图片描述
但是,在pos位置插入时是有特例的,即pos为单链表的第一个元素时,需要将plist的值改为plist,再将newnode->next的值改为pos即可:
在这里插入图片描述

// 单链表在pos位置插入x
void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{
	SListNode* newnode = BuySListNode(x);
	SListNode* beforepos = *pplist;
	if (beforepos == pos)
	{
		*pplist = newnode;
		newnode->next = pos;
	}
	else
	{
		while (beforepos->next)
		{
			if (beforepos->next == pos)
			{
				beforepos->next = newnode;
				newnode->next = pos;
				break;
			}
			beforepos = beforepos->next;
		}
	}
}

删除

当我们得到pos前的结点的地址后,就可以通过相同的方式实现删除pos位置的值:将beforepos->next的值改为beforepos->next位置的值,然后再释放pos后面的一块空间:
在这里插入图片描述

但是,有一种特例,即pos指向的是单链表的第一个元素时,我们只需要将plist的值改为pos->next;再将pos指向的空间释放即可:
在这里插入图片描述

// 单链表删除pos位置的值
void SListErase(SListNode** pplist, SListNode* pos)
{
	SListNode* beforepos = *pplist;
	if (beforepos == pos)
	{
		*pplist = pos->next;
		free(pos);
		pos = NULL;
	}
	else
	{
		while (beforepos->next)
		{
			if (beforepos->next == pos)
			{
				beforepos->next = pos->next;
				free(pos);
				pos = NULL;
				break;
			}
			beforepos = beforepos->next;
		}
	}
}

总结

到此,关于单链表的相关知识就介绍完毕了。当然,单链表是最简单的一种链表类型,在后面我们还会介绍一种比较复杂的链表,即带头双向循环链表。
当然,在介绍带头双向循环链表之前,我会先用一篇文章来讲解一些单链表的题目,欢迎大家持续关注哦

如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦

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

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

相关文章

K8s:渐进式入门服务网格 Istio (一)

写在前面 分享一些 Istio 的学习笔记博文内容涉及: istio 下载安装一个 Demo 运行什么是 istio,服务网格等概念介绍istio 架构组成,应用场景等 理解不足小伙伴帮忙指正 对每个人而言,真正的职责只有一个:找到自我。然后…

一文吃透 Go 内置 RPC 原理

hello 大家好呀,我是小楼,这是系列文《Go底层原理剖析》的第三篇,依旧分析 Http 模块。我们今天来看 Go内置的 RPC。说起 RPC 大家想到的一般是框架,Go 作为编程语言竟然还内置了 RPC,着实让我有些吃鲸。 从一个 Demo …

原型模式学习

本文讲解一下原型模式的概念并通过一个案例来进行实现。 4、原型模式 通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式原型模式就是Java中的克隆技术,以某个对象为原型,复制出新的对象,新的对象具有原…

VS2019加载解决方案时不能自动打开之前的文档(回忆消失)

✏️作者:枫霜剑客 📋系列专栏:C实战宝典 🌲上一篇: 错误error c3861 :“_T“:找不到标识符 逐梦编程,让中华屹立世界之巅。 简单的事情重复做,重复的事情用心做,用心的事情坚持做; 文章目录前言一、问题描…

借助ChatGPT爆火,股价暴涨又暴跌后,C3.ai仍面临巨大风险

来源:猛兽财经 作者:猛兽财经 C3.ai的股价 作为一家人工智能技术提供商,C3.ai(AI)的股价曾在2021年初随着炒作情绪的增加,达到了历史最高点,但自那以后其股价就下跌了90%,而且炒作情…

数据正确性验证(造数据篇)

变更记录 记录每次修订的内容,方便追溯。 多行文本单选作者日期完成文档V1.02023-02-27V1.1V1.2 1. 数据质量检测标准 1.1 背景:整理数据质量测试的维度 摘取自国标文档 以上是除了常规的软件质量模型外(软件测试质量六大特性&#xff0c…

Mysql Nested-Loop Join算法和MRR

MySQL8之前仅支持一种join 算法—— nested loop,在 MySQL8 中推出了一种新的算法 hash join,比 nested loop 更加高效。(后面有时间介绍这种join算法) 1、mysql驱动表与被驱动表及join优化 先了解在join连接时哪个表是驱动表&a…

ChatGPT今日正式开放API服务中小企业

开放隐私计算 开放隐私计算开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神,专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播,愿成为中国 “隐私计算最后一公里的服务区”。183篇原创内容公众号…

不要以没时间来说测试用例写不好

工作当中,总会有人为自己的测试用例写得不够好去找各种理由,时间不够是我印象当中涉及到最多的,也是最反感。想写好测试用例,前提是测试分析和需求拆解做的足够好,通过xmind或者UML图把需求和开发设计提供的产品信息提炼出来。 我个人的提炼标准一般是&…

CSS——学成在线案例

🍓个人主页:bit.. 🍒系列专栏:Linux(Ubuntu)入门必看 C语言刷题 数据结构与算法 HTML和CSS3 目录 1.案例准备工作 2.CSS属性书写顺序(重点) 3.页面布局整体思路 4.头部的制作​编辑 5.banner制作…

专治Java底子差,不要再认为泛型就是一对尖括号了

文章目录一、泛型1.1 泛型概述1.2 集合泛型的使用1.2.1 未使用泛型1.2.2 使用泛型1.3 泛型类1.3.1 泛型类的使用1.2.2 泛型类的继承1.4 泛型方法1.5 泛型通配符1.5.1 通配符的使用1)参数列表带有泛型2)泛型通配符1.5.2 泛型上下边界1.6 泛型的擦除1.6.1 …

只知道ChatGPT?这些AI工具同样值得收藏

B站|公众号:啥都会一点的研究生 人工智能革命带来了许多能够提高生产力和转变工作方式的工具,本期将重点介绍音频、视频、设计以及图像和数据清理中的顶级 AI 工具。 音视频类AI工具: VoicePen AI https://voicepen.ai:该工具可…

【内网服务通过跳板机和公网通信】花生壳内网穿透+Nginx内网转发+mqtt服务搭建

问题:服务不能暴露公网 客户的主机不能连外网,服务MQTT服务部署在内网。记做:p1 (computer 1)堡垒机(跳板机)可以连外网,内网IP 和 MQTT服务在同一个网段。记做:p2 (computer 2)对他人而言&…

linux 中的log

linux 中的log 由于内核的特殊性,我们不能使用常规的方法查看内核的信息。下面介绍几种方法。 1 printk()打印内核消息。 2 管理内核内存的daemon(守护进程) Linux系统当中最流行的日志记录器是Sysklogd,Sysklogd 日志记录器由…

【C++】位图

文章目录位图概念位图操作位图代码位图应用位图概念 boss直接登场: 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中❓ 40亿个整数,大概就是16GB。40亿个字节大概就是4GB。 1Byt…

sklearn中的降维算法PCA和SVD

目录 一.维度 二.sklearn中的降维算法 三.PCA与SVD 四.降维的实现 五.重要参数n_components 1.累积可解释方差贡献率曲线选择n_components 2.最大似然估计自选超参数 3.按信息量占比选超参数 六.PCA中的SVD 七.重要参数svd_solver 与 random_state 八.重要属性compon…

FormData同时传输多个文件和其他数据

近日有个需求是:在web的对话框中,用户可以输入文本内容和上传附件,附件的数量不限,所有附件总和大小不超过20M。 这个实现的方法不止一种,比如之前的后端同事是要求:文件和文本分开传输,文件用…

程序员的上帝视角(2)——我所体悟的思维方式

心外无物仍然记得在高中阶段,总是为了没有解题思路而苦恼。现在回想起来,总算有点感悟——执着于做题、刷题,却忽视了最本质的思考,为什么可以有这样的解题思路,别人是如何想到这种解题思路的。这正是心学所提倡的&…

189、【动态规划】leetcode ——312. 戳气球(C++版本)

题目描述 原题链接:312. 戳气球 解题思路 (1)回溯法 很多求最值实际上就是穷举所有情况,对比找出最值。因为不同的戳气球顺序会产生不一样的结果,所以实际上这就是一个全排列问题。 class Solution { public:int r…

linux shell 入门学习笔记18 函数开发

概念 函数就是将你需要执行的shell命令组合起来,组成一个函数体。一个完整的函数包括函数头和函数体,其中函数名就是函数的名字。 优点 将相同的程序,定义,封装为一个函数,能减少程序的代码数量,提高开发…