【数据结构初阶】 --- 单链表
关于链表你应该先了解这些
下图描述了物理模型和逻辑模型,大多数常见的其实是逻辑模型,但这对初学者或者掌握不扎实的同学不太友好,所以这里我重点讲解物理模型,当了解了这些细节,以后做题或是什么就直接画逻辑模型就好了

物理模型:
在这里,一个大方框表示着一个结点,这个结点里存储了两种数据:
- 一是你本身要存储的数据
- 二是为了让这些结点具有连接作用,而需要存放相应结点的地址,这样,当你访问了当前的结点内容,想要访问下一个结点的内容,这时就需要有下一个节点的地址,这就是为什么要存地址的原因。
链表的类型
单链表
上面的就是单链表
双链表

与单链表的不同就是一个结点里存有两个地址,可以实现双向访问,弥补了单链表只能单向访问的缺点
循环链表

单链表的最后一个结点存的是NULL,而循环链表的最后一个结点存放的是第一个结点的地址,事实上,你看这张图,确实也分辨不出哪个是第一个结点,所以想象成单链表最后一个结点放的头结点的地址就行
链表的存储方式
数组是一块连续的内存,而链表不同,每个结点都有自己的地址,并没有联系,这些地址是取决于操作系统的内存管理(在没学习操作系统之前可以当做这些地址是随机分配的)
链表的定义
这一节,我所讲的知识都是基于单链表
typedef int SLTDataType;//结点中存储数据的类型
typedef struct SListNode
{SLTDataType data;//结点中要存储的数据struct SListNode* next;//结点中指向下一个结点的指针
}SLTNode;
先看这张图:

与开头的那张区别就在于头指针phead,这是一个指针变量,用来存放第一个结点的地址,利用该指针就可以依次访问或操作节点中的数据
接下来初始化一个指向结点的头指针:(这里是头指针,不是头结点)
SLTNode* phead = NULL;
链表的操作
先了解是如何访问链表的




完整流程:

为什么要用二级指针?



经过了头插操作,我们将new_node成功连入链表的头部,pphead存的也是new_node的地址,但是,我们的实参phead,它里面的内容竟还是之前第一个结点的内容,而往后我们还是要用phead来访问操作这个链表但新插入的节点却永远访问不到,那你说能不能用pphead访问不就行了,记住,pphead是局部变量,执行完头插这个函数pphead就不存在了,我们想操作这个链表,一直都是利用phead当做实参传递的。想要改变实参的内容只需传址调用即可,因此,我们将指针变量phead的地址当做实参传递过去,那么相应的,形参就需要一个二级指针接收,因为phead是指针,我们传递的是指针的地址,所以需要二级指针接收。如此,通过对二级指针pphead解引用就可以直接对phead的内容进行修改,到此,phead就可以存new_node地址了
创建一个新节点
SLTNode* SLTCreatNode(SLTDataType x)
{
//里用malloc函数向栈区开辟一个大小为sizeof(SLTNode)个字节的空间,将这个空间的首地址存放于new_p这个指针变量中SLTNode* new_p = (SLTNode*)malloc(sizeof(SLTNode));malloc函数开辟空间失败会返回NULL,这时就不要再进行后续操作,避免解引用空指针if (new_p == NULL){perror("malloc");//进行报错,在屏幕上会出现错误提示exit(0);//直接让程序结束}//开辟成功就将x存入新的节点中,节点中的指针next指向NULLnew_p->data = x;new_p->next = NULL;return new_p;
}
打印链表
这里只需将头指针的内容作为实参穿过来,利用形参指针phead接收就能达到效果
void SLTPrint(SLTNode* phead)//这里接收的是第一个结点的地址
{while (phead != NULL){printf("%d->", phead->data);phead = phead->next;}printf("NULL\n");
}
头插法
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead != NULL);SLTNode* p_node = SLTCreatNode(x);p_node->next = *pphead;*pphead = p_node;
}
尾插法
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{//pphead里存的是phead的地址,不可能为NULL,所以如果传来NULL就会出问题,因此需要防止误传NULL指针,这里就要断言一下assert(pphead != NULL);SLTNode* p_node = SLTCreatNode(x);//试着将空链表的逻辑带入else中,你会发现cur->next在访问空指针,这是不行的,所以要专门为链表为空这个特殊情况写一段代码if (*pphead == NULL){*pphead = p_node;}else{SLTNode* cur = *pphead;while (cur->next != NULL){cur = cur->next;}cur->next = p_node;}
}
头删法
void SLTPopFront(SLTNode** pphead)
{assert(pphead != NULL);assert(*pphead != NULL);//如果传来的是个空链表就报错SLTNode* tmp = *pphead;*pphead = (*pphead)->next;free(tmp);
}
尾删法
//尾删的时候分三种情况讨论,没有节点,一个节点,多个节点
//不是说上来你就知道要分三种情况,而是当你写出适应多个节点的代码后
//这时你就要考虑边界情况,没有节点或者一个还是两个节点的情况
void SLTPopBack(SLTNode** pphead)
{assert(pphead != NULL);assert(*pphead != NULL);//头指针为空(没有节点)就报错if ((*pphead)->next == NULL)//只有一个节点的情况{free(*pphead);*pphead = NULL;}else//多个节点{SLTNode* cur = *pphead;SLTNode* prev = NULL;while (cur->next != NULL){prev = cur;cur = cur->next;}prev->next = NULL;free(cur);}
}
给指定的地址插入数据
这里是要将数据插入到pos前,为什么我们会知道一个结点的地址pos呢,答案就是查找函数帮你找的
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead != NULL);assert(pos != NULL);assert(*pphead);//传过来头指针指向的是空SLTNode* p_new = SLTCreatNode(x);if (*pphead == pos){p_new->next = *pphead;*pphead = p_new;}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}p_new->next = prev->next;prev->next = p_new;
查找数据
返回的是查找到结点的地址
SLTNode* SLTFind(SLTNode* phead,SLTDataType x)
{assert(phead != NULL);SLTNode* cur = phead;while (cur != NULL){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}
销毁链表(意外的知识)
在C语言和C++中程序员在堆区开辟的数据需要程序员亲手释放,一个一个开出的节点,只能一个一个释放掉。
这里既可以用一级指针也可以用二级指针,不过有个小小的区别,当你用free释放一个指针的动态内存后需要及时置空,那么在这里,我们的头指针phead在释放完链表后也需要及时置空
但是,
- 如果你传的实参是phead本身,那当SLTDestory执行完后,你需要额外的将phead手动置空
- 如果传的是phead的地址,那么在函数SLTDestory中,释放完链表后,就可以操作*pphead = NULL,其实就等于在函数内部将函数外部的phead置空,出了函数,就不需要手动置空了
- 到这里或许是有些抽象,你可以想一下free函数,它把指针释放后,需要你手动置空,实际上原因就是你传的是指针本身,free函数不能在它的内部将这个指针置空,所以需要你在free执行完手动置空
void SLTDestory(SLTNode* phead)
{SLTNode* cur = phead;while (cur != NULL){SLTNode* nxt = cur->next;free(cur);cur = nxt;}
}
void test()
{SLTNode* phead = NULL;...SLTDestory(phead);phead = NULL;//手动置空
}
void SLTDestory(SLTNode** pphead)//用二级指针接收phead的地址
{SLTNode* cur = *pphead;while(cur != NULL){SLTNode* nxt = cur->next;free(cur);cur = nxt;}*phead = NULL;//远程操作置空
}
void test()
{LSTNode* phead = NULL;...SLTDestory(&phead);//实参是指针变量phead的地址
}
哨兵位

- 哨兵位也算是一个结点,但哨兵位是不计入链表的长度。
- 哨兵位里不需要存值,最好不要存,有的数据结构书上会将哨兵位存入链表的结点个数,但这里的前提是你创建的链表是存储int型的数据,如果是char呢,那么根据数据在内存中的存储方式不同,char型只能存储8个bit的数据,范围也就是-128~127,如果这个链表结点个数大于127,那么这个哨兵位存的数据就会出错,如果换做是浮点型,那就更离谱了,所以哨兵位最好不要存储数据。
初始化
LSTNode* phead = (LSTNode*)malloc(sizeof(LSTNode));
//phead这个指针变量指向哨兵位
传参
有了哨兵位,再也不用为二级指针苦恼了
每当我们可能对链表的头结点进行插入或者删除(实际是phead指向的结点的地址改变了,phead的内容需要更改,只能通过二级指针的方式远程操控phead)
现在哨兵位出现了,你想对链表的头结点进行操作,想改变头结点的地址,随便改,我phead现在指向是哨兵位,哨兵位的地址是固定不变的,再也不用担心要修改链表的同时还要考虑需不需要修改phead的指向,当哨兵位不改变时,链表怎样的增删改,都只会影响哨兵位的指向,与phead无关
这里用头删演示:
void SLTPopFront(SLTNode* phead)
{assert(phead != NULL);assert(phead->next != NULL);//当链表为空还要删就报错SLTNode* tail = phead->next;phead->next = tail->next;free(tail);
}
为什么都是用头删,头插演示呢,
- 那是因为这两个操作更改的是第一个结点,节点发生改变,那就要更新指向这个结点的指针,现在的函数实参传的是哨兵位的地址,phead指向的也就是哨兵位,传的是哨兵位的地址那就可以对哨兵位进行更新,而远在函数外的头指针,一直指向的都是固定不变的哨兵位的地址。
- 而前面我们如果更改了第一个结点,那就要更新头指针,想在函数里更改函数外的指针只能传指针的地址,指针的地址就需要二级指针接收。
- 当然,重点讲带头单链表的原因也在于算法题里的链表大部分都是带头链表,避免与哨兵位混淆。哨兵位在某些时刻也会帮忙简化一些判断甚至帮助解题,后续有机会讲算法题会提到的。
相关文章:
【数据结构初阶】 --- 单链表
关于链表你应该先了解这些 下图描述了物理模型和逻辑模型,大多数常见的其实是逻辑模型,但这对初学者或者掌握不扎实的同学不太友好,所以这里我重点讲解物理模型,当了解了这些细节,以后做题或是什么就直接画逻辑模型就…...
并发、多线程、HTTP连接数有何关系?
在计算机领域,"并发"、"多线程"和"HTTP连接数"是三个重要的概念,它们之间存在着密切的关系。本文将探讨这三者之间的联系以及它们在现代计算机系统中的作用。 一、并发的概念 并发是指系统能够同时处理多个任务或事件的能…...
鸿蒙轻内核Kconfig使用笔记
鸿蒙轻内核使用Kconfig进行图形化配置,本文专门讲解下鸿蒙轻内核LiteOS-M和LiteOS-A的图形化配置方法。本文中所涉及的源码,均可以在开源站点 https://gitee.com/openharmony/kernel_liteos_a 、 https://gitee.com/openharmony/kernel_liteos_m 获取。本…...
react 0至1 案例
/*** 导航 Tab 的渲染和操作** 1. 渲染导航 Tab 和高亮* 2. 评论列表排序* 最热 > 喜欢数量降序* 最新 > 创建时间降序* 1.点击记录当前type* 2.通过记录type和当前list中的type 匹配*/ import ./App.scss import avatar from ./images/bozai.png import {useState} …...
基于MCU平台的HMI开发的性能优化与实战(上)
随着汽车座舱智能化的不断演进,车内显示设备的数量显著增加,从传统的仪表盘和中控屏扩展至空调控制、扶手、副驾驶区域以及抬头显示(HUD)等多样化的显示单元。为了有效支持这些功能单元,同时控制整车成本,越…...
【Tkinter界面】Canvas 图形绘制(02/5)
文章目录 一、说明二、几何时使用 Canvas 组件2.1 用法2.2 简单范例2.3 对象移动2.4 对象删除2.5 文字对象显示 三、画布和画布对象3.1 画布生成函数原型3.2 使用create_xxx()方法3.3 对参数**options的解释 一、说明 Canvas(画布)组件为 Tkinter 的图形…...
1_常见指令【Linux中常见30个指令的学习和使用】【万字长文】
常见指令以及权限理解 开始学习linux前的注意事项 在学习linux之前,我们要知道linux是一个操作系统。 那操作系统是什么呢?(这里只做大概了解) 操作系统就是一个管理软硬件的软件。 它对上提供良好(稳定、高效、安…...
每日复盘-202406014
今日关注: 这几天市场打板情绪环境转好,轻仓试错 20240614 六日涨幅最大: ------1--------301036--------- 双乐股份 五日涨幅最大: ------1--------301036--------- 双乐股份 四日涨幅最大: ------1--------301036--------- 双乐股份 三日涨幅最大: ------1--------301082-…...
JavaScript 深拷贝和浅拷贝的实现、使用场景和存在的问题
浅拷贝 实现 方式 1(ES 5 语法): const params Object.assign({}, state.dataForm)方式 2(ES 6 语法): const params { ...state.dataForm }使用场景 copy 入参和出参 深拷贝 方式 1(手…...
8个常用的辅助函数!!
在开发各种项目时,我们会发现经常需要一些辅助函数来帮助我们实现一些需求,并且这些函数是在很多项目里都可以进行复用的。下面我就列出我们一些常用的辅助函数,来帮助大家在开发项目时,进行复用。 1. 首字母大写 将字符串的第一…...
服务器数据恢复—OceanStor存储中NAS卷数据丢失如何恢复数据?
服务器存储数据恢复环境&故障: 华为OceanStor某型号存储。工作人员在上传数据时发现该存储上一个NAS卷数据丢失,管理员随即关闭系统应用,停止上传数据。这个丢失数据的卷中主要数据类型为office文件、PDF文档、图片文件(JPG、…...
54.Python-web框架-Django-免费模板django-datta-able
1.Datta Able Django介绍 Detta Able Djiango是什么 Datta Able Django 是一个由AppSeed提供的开源Django管理面板,基于现代设计,为开发者提供了一流的功能和优雅的界面。它源自CodedThemes的高风格化Bootstrap 4模板——Datta Able Bootstrap Lite&…...
XP系统安装Node.js v8.6.0并搭建Vue2开发环境(项目兼容到Vista的IE9浏览器)
下载并安装Node.js v8.6.0 通常我们开发Vue2项目,是通过vue create命令建立Vue2工程,用npm run serve命令启动Vue2网站的。 vue命令是用JavaScript写的,不是用C语言写的,必须要Node.js环境才能运行,由Node.js自带的np…...
redis序列化
文章目录 1、为什么要进行序列化操作?2、序列化方式2.1、自定义序列化2. 2、StringRedisTemplate(重点) 1、为什么要进行序列化操作? 不进行序列化向redis存入数据代码: SpringBootTest class RedisDemoApplicationT…...
IOT-Tree 1.7.0实现了一个类似Node-Red的流程功能
本人一直研究这个软件,1.7.0版本最近刚刚发布,里面有个大变化,增加了消息流的功能,这个功能和IBM的Node-Red很相似。 Node-Red那个图形化流程很多年前就给了我很深刻的印象,我个人理解是,通过这样的图形化…...
nc网络收发测试-tcp客户端\TCP服务器\UDP\UDP广播
netcat(nc): 作用:一个功能强大的网络工具,提供了简单的网络测试和网络编程功能。工作原理:可以用于建立TCP或UDP连接,并发送和接收数据。示例用法: 监听TCP端口:nc -l 1…...
程序员该有怎么样的职业素养
目录 1、持续学习 2、解决问题的能力 3、团队协作能力 4、责任感 5、沟通能力 6、总结 作为一个从业者,我认为对于程序员而言,职业素养是非常重要的。职业素养不仅影响个人的职业发展,也影响团队和企业的整体氛围和效率。在我的职业生涯…...
51交通灯
一、基本原理 利用51单片机控制各个路口红绿灯及时间显示。 设计的重点: 1、各个路口红绿灯亮灭的规则,暂不考虑左转方向; 2、倒计时的实现,利用单片机的定时器进行计数得到秒信号; 3、时间显示:东西南…...
鸿蒙Arkts上传图片并获取接口返回信息
需求: 选择相册图片后,将文件上传到服务器,接口会返回图片地址。 问题: 1、鸿蒙自带的文件上传返回值只会返回上传状态,不会返回接口返回信息。 类似问题 HarmonyOS上传文件以及权限授权_harmonyos中axios上传文件…...
超文本标记语言(HTML)简介
HTML 基础 超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用来结构化 Web 网页及其内容的标记语言。网页内容可以是:一组段落、一个重点信息列表、也可以含有图片和数据表。正如标题所示…...
eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...
【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
多模态图像修复系统:基于深度学习的图片修复实现
多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...
LabVIEW双光子成像系统技术
双光子成像技术的核心特性 双光子成像通过双低能量光子协同激发机制,展现出显著的技术优势: 深层组织穿透能力:适用于活体组织深度成像 高分辨率观测性能:满足微观结构的精细研究需求 低光毒性特点:减少对样本的损伤…...
作为测试我们应该关注redis哪些方面
1、功能测试 数据结构操作:验证字符串、列表、哈希、集合和有序的基本操作是否正确 持久化:测试aof和aof持久化机制,确保数据在开启后正确恢复。 事务:检查事务的原子性和回滚机制。 发布订阅:确保消息正确传递。 2、性…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...
