详解单链表(内有精美图示哦)
全文目录
- 引言
- 链表
- 链表的定义与结构
- 链表的分类
- 单链表的实现及对数据的操作
- 单链表的创建与销毁
- 创建
- 销毁
- 单链表的打印
- 单链表的头插与头删
- 头插
- 头删
- 单链表的尾插与尾删
- 尾插
- 尾删
- 单链表的查找
- 单链表在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;}}
}
总结
到此,关于单链表的相关知识就介绍完毕了。当然,单链表是最简单的一种链表类型,在后面我们还会介绍一种比较复杂的链表,即带头双向循环链表。
当然,在介绍带头双向循环链表之前,我会先用一篇文章来讲解一些单链表的题目,欢迎大家持续关注哦
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦
相关文章:
详解单链表(内有精美图示哦)
全文目录引言链表链表的定义与结构链表的分类单链表的实现及对数据的操作单链表的创建与销毁创建销毁单链表的打印单链表的头插与头删头插头删单链表的尾插与尾删尾插尾删单链表的查找单链表在pos位置后插入/删除插入删除单链表在pos位置插入/删除插入删除总结引言 在上一篇文…...
csdn文章导航
这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…...
【Spring】掌握 Spring Validation 数据校验
个人简介:Java领域新星创作者;阿里云技术博主、星级博主、专家博主;正在Java学习的路上摸爬滚打,记录学习的过程~ 个人主页:.29.的博客 学习社区:进去逛一逛~ Spring Validation 数据校验一、什么是 Spring…...
定语 从句
回顾能作定语的成分 形容词:She is a responsible girl.她是一个负责任的姑娘。(前置定语) The girl responsible was expelled.对此负责的姑娘被开除了。(后置定语) 代词:Whose f…...
【数据可视化工具】浅谈 DataEase 和 FineBI 支持的数据源
前言最近对市面上比较火热的数据可视化工具 DataEase 和 FineBI 进行了调研,在支持的数据源方面感觉不太一样,所以就有了这篇文章,话不多说,我们一起来看一下吧!以下的内容,大多来自两个工具的官方文档&…...
100种思维模型之上帝视角思维模型-025
惊奇、愤怒、郁闷,我们觉得生活不精彩,事情乱作一团,但这仅仅是视角问题而已。 换个视角,可以看到不同的世界。 “上帝视角思维模型”,即以一个更高、更客观、更理性的角度来看问题,从而做出理性的决策。 …...
从这5个方面,总结我当PM的第一年
以下5个方面(学习、思考、沟通、执行、产品)的分享,都是我站在巨人的肩膀上,结合自己所学所做总结而来;同时,我也继续学习,不断完善这些知识。如有不当,欢迎大家指正~一、学习&#…...
ChatGPT可以作为一个翻译器吗?
论文地址:https://arxiv.org/abs/2301.08745.pdf 背景 自从OpenAI2022年11月30日发布ChatGPT以来,基本上把NLP所有任务大统一了,那么在机器翻译的表现到底如何呢?腾讯AI Lab在翻译Prompt、多语言翻译以及翻译鲁棒性三方面做了一…...
详述java的设计模式(三)
1.装饰者模式 装饰者模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。 使用场景: 在不影响其他对象的情况下ÿ…...
Linux命令·pwd
Linux中用 pwd 命令来查看”当前工作目录“的完整路径。 简单得说,每当你在终端进行操作时,你都会有一个当前工作目录。 在不太确定当前位置时,就会使用pwd来判定当前目录在文件系统内的确切位置。1.命令格式:pwd [选项…...
以图搜图服务快速搭建
以图搜图服务快速搭建 电商公司,管理的商品少则几千,多则上百万。如何帮助用户从多如牛毛的商品中找到类似的商品就成了问题。 以图搜图就可以很好的帮助解决这个问题,通过 Towhee(resnet50 模型) Milvus 如何实现本…...
【TensorFlow安装踩坑记录】
TensorFlow安装踩坑记录第一步,切换服务器cuda版本第二步,conda安装tensorflow记录一下最近安装Tensorflow v1时遇到的问题和解决办法第一步,切换服务器cuda版本 首先我想安装tensorflow 1.13.1,兼容的cuda版本是10.0,…...
03.03回溯法
class Solution { public:vector<int> temp;vector<vector<int>> ans;void dfs(int cur,int n,int k){//剪枝 temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 tempif(temp.size()(n-cur1)<k){return;}if(temp.size()k){ans…...
I.MX6ULL内核开发0:linux内核模块
目录 简要 一、内核模块的概念 二、内核模块加载、卸载过程 简要 1、内核模块的概念 2、内核模块的原理:内核模块在内核的加载、卸载过程。 一、内核模块的概念 内核,是一个操作系统的核心。是基于硬件的第一层软件扩充,提供操作系统的最…...
qsort快速排序的实现以及模拟实现qsort的功能(狠狠的拿捏)
当你为错过太阳而哭泣的时候,你也要再错过群星了。 --泰戈尔 目录 一.qsort快速排序的实现 二.模拟实现一个qsort功能的函数 一.qsort快速排序的实现 下面是 qsort() 函数的声明: void qsort(void *base, size_t nitems, size_t size, int (…...
[Java·算法·中等]LeetCode215. 数组中的第K个最大元素
每天一题,防止痴呆题目示例分析思路1题解1分析思路2题解2分析思路3题解3👉️ 力扣原文 题目 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。 请注意,你需要找的是数组排序后的第 k 个最大的元素,而不…...
xgboost:算法数学原理
xgboost算法数学原理 1、求预测值 y^iϕ(xi)∑k1Kfk(xi),fk∈F,(1)\hat{y}_i\phi\left(\mathbf{x}_i\right)\sum_{k1}^K f_k\left(\mathbf{x}_i\right), \quad f_k \in \mathcal{F},\tag{1} y^iϕ(xi)k1∑Kfk(xi),fk∈F,(1) F{f(x)wq(x)}(q:Rm→T,w∈RT)\mathca…...
map、multimap、unordered_map
引用:windows程序员面试指南 map map 红黑树 map 对value值无要求 map 有序,按照key值自动排序 map key值唯一 map 头文件:#include map 支持重载[]的运算符 map 为保持有序性,erase()开销大 multimap multimap 红黑树 multim…...
2023年全国最新会计专业技术资格精选真题及答案11
百分百题库提供会计专业技术资格考试试题、会计考试预测题、会计专业技术资格考试真题、会计证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 一、选择题 1.下列各项中,仅将生产过程中消耗的变动成本计入产品成本…...
Centos7搭建NFS
1.NFS简介Network File System(网络文件系统,通过网络让不同的机器系统之间可以彼此共享文件和目录,类似Samba服务。2.NFS挂载原理 在网络中服务器和客户端进行连接都是通过端口进行数据传输,而NFS服务端的端口是随机的,从而导致N…...
装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...
Golang——7、包与接口详解
包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...
从面试角度回答Android中ContentProvider启动原理
Android中ContentProvider原理的面试角度解析,分为已启动和未启动两种场景: 一、ContentProvider已启动的情况 1. 核心流程 触发条件:当其他组件(如Activity、Service)通过ContentR…...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...
