数据结构_单链表

今天我们要开启链表的学习
🖋️🖋️🖋️
学了顺序表我们可以知道:
🎈链表其实就是争对顺序表的缺点来设计的,补足的就是顺序表的缺点
🎈链表在物理上是上一个节点存放的下一个节点的地址
链表
1.链表的概念及结构
1.1概念
概念:链表是一种
物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
1.2结构

根据上图我们可以知道:
链表是由一系列节点组成的线性数据结构,每个节点包含数据部分和指向下一个节点的指针(在双向链表中还有指向前一个节点的指针)。节点通过指针连接在一起,形成一个链式结构,数据在内存中存储并不连续。
2.链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1.单向、双向
2.带头、不带头
3.循环、非循环
(这里图就不展示啦,大家感兴趣的可以自己去查找噢)
3.链表的实现
1.创建一个节点
typedef int SListDataType;
//节点
typedef struct SListNode
{//定义一个数据SListDataType data;//这个data就是下图中的1 2 3//定义一个指针struct SListNode* next;//next就是存放的指针
}SListNode;
这部分代码定义了一个单链表的节点结构:
struct SListNode:定义了一个名为 SListNode 的结构体,用于表示单链表中的一个节点。SListDataType data;:在结构体中定义了一个成员变量data,其数据类型为之前定义的SListDataType(这里实际上就是 int 类型)。这个成员变量用于存储链表节点所包含的数据。struct SListNode* next;:定义了一个指向struct SListNode类型的指针next。这个指针用于指向下一个链表节点,通过它可以将多个节点连接起来形成链表。
2.遍历打印链表
void SListPrint(SListNode* phead)
{//这里不需要assert断言,因为这里为空的话就是空链表SListNode* cur = phead;//指向第一个节点while (cur != NULL){printf("%d ", cur->data);cur = cur->next; }
}
画图解释:

3.尾插
//动态申请新节点并初始化
SListNode* BuySListNode(SListDataType x)
{SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));//在内存的堆区动态分配一块连续的内存空间,大小为 SListNode 结构体的字节数,再强转为SListNode*型if (newNode == NULL){printf("申请结点失败\n");exit(-1);}newNode->data = x;//将传入的数据 x 存储到新节点的 data 成员newNode->next = NULL;//初始化新节点的 next 指针为 NULL(新节点暂时无后续节点)return newNode;//返回创建好的节点地址
}//尾插
void SListPushBack(SListNode** pphead, SListDataType x)
{SListNode* newNode = BuySListNode(x);//调用函数创建待插入的新节点if (*pphead == NULL){*pphead = newNode;//若链表为空,直接让头指针指向新节点}else{//找尾SListNode* tail = *pphead;//定义指针 tail 指向链表头部while (tail->next != NULL)//遍历链表,找到尾节点(尾节点的 next 为 NULL){tail = tail->next;//实现遍历链表的关键代码,将指针 tail 移动到下一个节点}tail->next = newNode;//将尾节点的 next 指针指向新节点,完成尾插}}
问题:动态申请return newNode;为什么要返回创建好的节点地址
返回新节点的地址是为了让调用者能够:
- 将
节点插入链表中。 - 后续操作节点的数据或指针。
- 正确
释放内存(避免泄漏)。
这是 C 语言中动态内存管理的标准做法,确保函数间的数据传递和内存控制权的转移。
4.尾删
//尾删
void SListPopBack(SListNode** pphead)
{//1.空//2.一个结点//3.一个以上结点// 处理空链表if (*pphead == NULL){return;//直接返回,没得删}// 处理单节点链表else if ((*pphead)->next ==NULL){free(*pphead);*pphead = NULL;//只有这一个就直接删}// 处理多节点链表else{SListNode* prev = NULL;//用于记录尾节点的前驱节点SListNode* tail = *pphead;//用于遍历链表while (tail->next != NULL){prev = tail;//在循环中,prev 始终指向 tail 的前一个节点,tail 逐步后移tail = tail->next;//再往后走}free(tail);tail = NULL;prev->next = NULL;//将其存放的地址置空,避免内存泄漏和悬空指针}
}
5.头插
//头插
void SListPushFront(SListNode** pphead, SListDataType x)
{SListNode* newnode = BuySListNode(x);//创建新节点newnode->next = *pphead;//新节点的 next 指向原头节点*pphead = newnode;//更新头指针为新节点
}
步骤分析:
- 步骤 1:创建新节点
调用BuySListNode(x):
动态分配内存,初始化data为x,next为NULL。 - 步骤 2:连接新节点与原头节点
newnode->next = *pphead;:
若原链表非空,新节点的next指向原头节点(形成链式关系)。
若原链表为空(*pphead == NULL),新节点的next仍为NULL。 - 步骤 3:更新头指针
*pphead = newnode;:
将头指针pphead指向新节点,使其成为链表的新头部。
画图解释:

注意:
- 核心原因:头指针是链表的入口,必须指向第一个节点。头插法中,新节点成为第一个节点,因此必须更新头指针。
- 技术实现:通过双重指针修改原始头指针,确保链表结构正确。若省略此步骤,新节点将无法被链表访问,导致内存泄漏或逻辑错误。
6.头删
//头删
void SListPopFront(SListNode** pphead)
{//1.空//2.一个节点 + 3.一个以上节点if (*pphead == NULL){return;}else{SListNode* next = (*pphead)->next;//保存头节点的下一个节点 free(*pphead);// 释放头节点内存*pphead = next;// 更新头指针为原头节点的下一个节点 }
}
详细分析:
- 保存后续节点:
SListNode* next = (*pphead)->next;记录原头节点的下一个节点,防止链表断裂。 - 释放内存:
free(*pphead);释放原头节点的内存,避免内存泄漏。 - 更新头指针:
*pphead = next;使头指针指向原头节点的下一个节点,完成头删。
举个例子:

注意:
左边 next:是程序员自定义的指针变量,用于临时存储地址。右边 next:是链表节点结构体固有的成员,用于维系链表的链式结构。
二者仅名称相同,但一个是变量标识,一个是结构体成员,功能和性质完全不同。
7.单链表查找
// 单链表查找
SListNode* SListFind(SListNode* phead, SListDataType x)
{SListNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}
详细分析:
SListNode* cur = phead;定义一个指针cur并初始化为phead,用于遍历单链表。cur会从链表的头节点开始,依次向后移动;- 在循环内部,使用
if语句检查当前节点cur的数据域data是否等于要查找的值x。如果相等,则表示找到了目标节点,直接返回cur,即该节点的指针。 cur = cur->next;如果当前节点的数据域不等于 x,则将cur指针更新为指向下一个节点,继续向后遍历链表。return NULL;如果while循环结束后仍未找到值为x的节点,说明链表中不存在该值,此时返回NULL
在test函数中实现时:

最后的打印结果为:
(将3修改为了30)

8.单链表在pos位置之后插入x
void SListInsertAfter(SListNode* pos, SListDataType x)
{assert(pos);SListNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;
}
问题:分析思考为什么不在pos位置之前插入?
原因:单链表的单向性决定了插入操作必须依赖前驱节点的遍历,若pos无法通过遍历到达(如越界或无前驱),则无法实现插入。
实例:若链表为1→2→3,想在节点2前插入0,需先找到节点1(前驱),再修改其指针为1→0→2→3。若无法找到前驱(如pos为4),则插入失败。
关键指针操作分析
newnode->next = pos->next;
pos->next是pos节点原本指向的下一个节点的指针。
这行代码的作用是让新节点newnode的next指针指向pos节点原来的下一个节点。其目的是在插入新节点时,保证新节点能够连接到原链表中pos节点之后的部分,避免丢失后续的节点。可以把它理解为 “记住”pos节点后面的节点,以便后续正确连接链表。pos->next = newnode;
这行代码将pos节点的next指针指向新节点newnode。
结合上一步,这就完成了将新节点插入到 pos 节点之后的操作。此时,pos节点后面跟着新节点newnode,而新节点newnode后面接着原链表中 pos 节点原本的后续节点,链表结构更新成功。
画图分析:

9.单链表删除pos位置之后的值
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode* pos)
{assert(pos);if (pos->next)//不为空{SListNode* next = pos->next;SListNode* nextnext =next->next;pos->next = nextnext;free(next);}
}
分析思考为什么不删除pos位置?
原因:在单链表中,直接删除pos位置节点需要遍历链表来寻找前驱节点,这会增加代码的复杂度和时间成本。因此,通常建议优先采用删除后继节点或者复制后继节点值的替代方案。如果确实需要删除pos节点,就必须传递头指针的地址(即二级指针),以便在头节点被删除时能够正确更新链表的头指针。
画图分析:

详细分析:
🖋️🖋️🖋️
pos->next也就是方框1的后半个位置,存放的是next的地址;通过next指向的next也就是第二个next的后半个方框,存放的是第三个nextnext的地址;所以删除后直接将nextnext所存放的地址赋值给pos->next!
如何去调用呢?

10.接口实现

🎉🎉🎉
到这里本章就结束啦~
我们下节见~
相关文章:
数据结构_单链表
今天我们要开启链表的学习 🖋️🖋️🖋️ 学了顺序表我们可以知道: 🎈链表其实就是争对顺序表的缺点来设计的,补足的就是顺序表的缺点 🎈链表在物理上是上一个节点存放的下一个节点的地址 链表 …...
深陷帕金森困境,怎样重燃生活信心?
帕金森,这个悄然影响无数中老年人生活的神经系统疾病,正逐渐走进大众视野。患病后,患者常出现静止性震颤,安静时手部、下肢不自主抖动,如同在默默诉说着身体的异常。肢体变得僵硬,行动迟缓,起步…...
C语言(23)
字符串函数 11.strstr函数 1.1函数介绍: 头文件:string.h char *strstr ( const char * str1,const char *str2); 作用:在一个字符串(str1)中寻找另外一个字符串(str2)是否出现过 如果找到…...
Docker运行hello-world镜像失败或超时:Unable to find image ‘hello-world:latest‘ locally Trying to pull reposi
Docker运行hello-world镜像失败或超时,报错:Unable to find image ‘hello-world:latest’ locally Trying to pull repository docker.io/library/hello-world … /usr/bin/docker-current: missing signature key. See ‘/usr/bin/docker-current run …...
Linux内核如何和设备树协同工作的?
1.编写设备树 cd arch/riscv/boot/dts/ 再cd到厂商,例如下述内容。 2.编译设备树(dts->dtb)通过dtc命令来转换 3.解析设备树 例如上述内容,都是对设备树的解析。 这里重点说一下内核对设备树的处理吧,因为这个内…...
electron的通信方式(三种)
文章目录 一、渲染进程向主进程发送消息二、渲染进程向主进程发送消息并异步获取结果三、主进程向渲染进程发送消息 electron的主要是主线程和渲染线程之间的通信,简单记录一下三种通信方式 一、渲染进程向主进程发送消息 利用ipcRenderer.send()和ipcMain.on()方法…...
LLM中的transformer结构学习(二 完结 Multi-Head Attention、Encoder、Decoder)
文章目录 LLM中的transformer结构学习(二 完结 Multi-Head Attention、Encoder、Decoder)Self-Attention (自注意力机制)结构多头注意力 EncoderAdd & Norm 层Feed Forward 层 EncoderDecoder的第一个Multi-Head AttentionMas…...
高效编程指南:PyCharm与DeepSeek的完美结合
DeepSeek接入Pycharm 前几天DeepSeek的充值窗口又悄悄的开放了,这也就意味着我们又可以丝滑的使用DeepSeek的API进行各种辅助性工作了。本文我们来聊聊如何在代码编辑器中使用DeepSeek自动生成代码。 注:本文适用于所有的JetBrains开发工具,…...
嵌入式c学习四
c语言的输入输出:ANSI组织发布的标准c库,使用函数需要包含对应头文件,使用输入输出函数时需要包含stdio.h (stdio -> standard input output)标准输入输出 格式化输出:int printf(const char * restrict format, ...)…...
爱可以传递,幸福可以具象化
遇到什么:晚上上课学生吵吵吵,把学生手机全部收了,放讲台上。 感受到的情绪:很烦躁。 反思:收手机也不是长久之计,可是物理有什么翻转课堂呢? 明天的待办事项:早上高数选修课&#x…...
力扣-数组-367 有效的完全平方数
思路和时间复杂度 思路:利用二分,确定区间是左闭右闭,然后根据大小进行二分时间复杂度: 代码 class Solution { public:bool isPerfectSquare(int num) {bool flag false;if(num 0 || num 1) return true;long long …...
Java关键字与标识符
Java关键字是预定义的保留字,用于定义程序结构和语义,如if、for、class等,不能用作标识符。JDK 8有50个关键字,JDK 11引入var用于局部变量类型推断。标识符用于命名变量、类等,由字母、数字、_、$组成,不能…...
【神经网络】python实现神经网络(二)——正向推理的模拟演练
一.神经网络假设 在开始讲解之前,首先我们假设有这样一套神经网络,一共有三层: 其中,关于神经网络的权重、偏置的符号定义如下(如果不知道什么是权重和偏置,可以参考我之前写过的一篇文章:【机器学习】机器学习是什么意思): 以下文章将沿用以上这个设…...
DeepSeek模型本地化部署方案及Python实现
DeepSeek实在是太火了,虽然经过扩容和调整,但反应依旧不稳定,甚至小圆圈转半天最后却提示“服务器繁忙,请稍后再试。” 故此,本文通过讲解在本地部署 DeepSeek并配合python代码实现,让你零成本搭建自己的AI…...
Rust规律归纳随笔
1. 针对所有权规则: 唯一所有权 <------------> 智能指针(引用计数)<-------------->共享所有权 (引用计数) { 单线程:Rc 多线程:Arc } 2. 针对借用规则: 共享不可变(多读)<----->…...
Houdini SOP层 Scatter节点
SOP 代表 Surface Operator(几何体操作节点),所有几何体的建模、变形、分布等操作都在此层级完成。 Scatter节点的作用就是 以不同的密度在模型表面撒点 Scatter 节点属于 SOP(几何体)层级: 进入 Geometr…...
Win7 火狐浏览器 Mozilla Firefox 115.7.0esr下载地址(及Chrome、Supermium浏览器)
如题,官网下载地址: Mozilla Firefox 115.7.0esr 已经发布,感兴趣的朋友可去官方下载! https://ftp.mozilla.org/pub/firefox/releases/115.7.0esr/ 其他方案: 1、谷歌 Chrome 浏览器的 109版本,即最后一…...
Docker 学习笔记:从入门到部署,实战演练全流程!
📌 开篇:为什么要学 Docker? 还在为环境不一致、部署麻烦、依赖冲突头疼吗?Docker 让一切变得简单!作为现代开发和运维的神器,Docker 让我们可以用 一句命令 解决 “在我电脑上能跑” 的问题。今天&#x…...
【网络安全工程】任务12:网络安全设备
目录 一、防火墙 1、作用 2、配置方式 3、存在的漏洞 二、入侵检测系统(IDS)和入侵防御系统(IPS) 1、作用 2、配置方式 3、存在的漏洞 三、防病毒网关 1、作用 2、配置方式 3、存在的漏洞 …...
Linux 进程控制:创建、终止、等待与程序替换全解析
亲爱的读者朋友们😃,此文开启知识盛宴与思想碰撞🎉。 快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。 目录 1.进程创建 1-1 fork函数初识 1-2 fork函数返回值 1-3…...
SwiftUI 让视图自适应高度的 6 种方法(四)
概览 在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。 如上图所示,无论顶部 TabView 容器里子视图…...
机器学习中的梯度下降是什么意思?
梯度下降(Gradient Descent)是机器学习中一种常用的优化算法,用于最小化损失函数(Loss Function)。通过迭代调整模型参数,梯度下降帮助模型逐步逼近最优解,从而提升模型的性能。 1.核心思想 梯…...
opencv-显示图片
安装软件 sudo apt install python3 //确保虚拟机只有python3 ln -sf /usr/bin/python3.6 /usr/bin/python sudo apt install python3-pip pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple pip install opencv-contrib-python -i https://pypi.tuna…...
sap关账+策略模式(避免大量if elseif)
旧代码 Transactional(rollbackFor Exception.class)public AjaxResult purchaseOrderReceiptOutSourceAfterSapCloseAccountingPeriod(Long id) {SysPurorderPostingLog sysPurorderPostingLog sysPurorderPostingLogMapper.selectSysPurorderPostingLogById(id);if (Object…...
EverArt MCP 服务器安装调试笔记 -cline
EverArt MCP 服务器安装调试笔记 问题描述 用户在使用 EverArt MCP 服务器时遇到报错:“MCP error -1: Connection closed”。 调试过程 检查配置文件 cline_mcp_settings.json: 确认 everart 服务器的配置信息,包括 command、args 和 env 是否正确。…...
1035.不相交的线
1035.不相交的线 力扣题目链接(opens new window) 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足: nums1[i] nums2[j]且绘制的直线…...
Django-ORM-select_related
Django-ORM-select_related 作用使用场景示例无 select_related 的查询有 select_related 的查询 如何理解 "只发起一次查询,包含所有相关作者信息"1. select_related 的工作原理2. 具体示例解析3. 为什么只发起一次查询 数据库中的books量巨大࿰…...
2001-2023年上市公司数字化转型年报词频统计(年报词频统计和MDA词频统计两种方式)(吴非、赵宸宇、甄红线300+关键词三种方法)
2001-2023年上市公司数字化转型年报词频统计(年报词频统计和MD&A词频统计两种方式)(吴非、赵宸宇、甄红线300关键词三种方法) 1、时间:2001-2023年 2、来源:上市公司年报 3、参考文献: …...
IO多路复用实现并发服务器
一.select函数 select 的调用注意事项 在使用 select 函数时,需要注意以下几个关键点: 1. 参数的修改与拷贝 readfds 等参数是结果参数 : select 函数会直接修改传入的 fd_set(如 readfds、writefds 和 exceptfds…...
React 如何实现组件懒加载以及懒加载的底层机制
前言 在现代前端开发中,性能优化始终是一个核心课题。React 作为当下流行的前端库之一,提供了一些非常有用的工具和技术来提升应用的性能,其中懒加载(Lazy Loading)就是一项不可忽视的重要技术。通过懒加载࿰…...
