数据结构入门指南:链表(新手避坑指南)
目录
前言
1.链表
1.1链表的概念
1.2链表的分类
1.2.1单向或双向
1.2.2.带头或者不带头
1.2.33. 循环或者非循环
1.3链表的实现
定义链表
总结
前言
前边我们学习了顺序表,顺序表是数据结构中最简单的一种线性数据结构,今天我们来学习链表,难度相较于顺序表会大幅增加,非常考验大家对结构体、指针的理解。但是也不要害怕,我会一一向大家解答疑惑,本期的内容先给初学者预预热,主要介绍在刚开始学习链表时需要注意的点、涉及的基础知识以及逻辑基础,下期会将功能接口具体实现。
1.链表
1.1链表的概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
逻辑图:

而现实中的链表是这样的:

通过图我们可以观察到,链式数据结构在逻辑上是连续的,在物理上不一定连续。链表的好处在于对内存更高效的使用(加入一个节点就开辟一个节点的空间)。
注意:
- 链表的节点是在堆上开辟的,程序不结束就不会主动释放。
- 从堆上申请的空间是按照一定策略分配的,两次申请的空间可能连续,也可能不连续。
1.2链表的分类
链表主要分为以下几种:
1.2.1单向或双向

单向的链表简称为单链表,单链表只可以进行单向遍历,而双向链表完美的弥补了这个缺陷,可以向前遍历也可以向后遍历
1.2.2带头或者不带头

它们本质上并没有太大的区别,在链表功能实现过程中,有头节点的不需要对特殊节点进行特殊操作,相对简单,但对于大部分的刷题网站上链表的题目都是默认为无头节点的,所以对于无头节点链表的理解更为重要。
1.2.3 循环或者非循环

循环链表可以用于解决一些特定的问题,非循环链表一般用于普通的链表操作,例如插入、删除、查找等。
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。
1.3链表的实现
对于初学者来说,我个人建议先从无头单向非循环链表学起,因为在很多的刷题场景中都是单项无头非循环链表,理解了它也可以帮助你更快的适应链表的刷题。今天我们主要从这种单链表讲起。
单链表的实现过程中会存在很多的坑,对于初学者来说是很困难的,我会一一列举帮助大家避免这些错误,刚开始我会从基础知识层面进行逐个分析,当然内容也会很多,我会分期进行讲解。
定义链表
typedef int Datatype;
typedef struct SLNode
{Datatype data;SLNode* next;
}SLNode;
定义链表这里就迎来了第一个坑,上述的这种形式很常见,对于初学者来说这里就有一个坑,这个链表定义的是否正确呢?
这种方式是不正确的,为什么?typedef是重命名,结构体是我们自定义类型,SLNode是重命名后的名字,但是这里需要注意,这个重命名是从上述代码第六行结束后才开始生效,在结构体中提前使用是不允许的。
正确的定义:
typedef int Datatype;
typedef struct SLNode
{Datatype data;struct SLNode* next;
}SLNode;
定义之后就需要创建节点,创建节点这里我们要明白:我们是通过函数的形式来创建节点,创建时顺便赋值,初学者或许会有这样的疑问:那为什么函数的类型是结构体指针类型?例如:
SLNode* NewNode(Datatype x)
{
}
为了后续节点的链接,我们最后需要返回新建节点的地址,而节点地址的类型就是SLNode* ,函数的类型也只是函数的返回类型。
接下来就是功能实现:
SLNode* NewNode(Datatype x)
{SLNode* newnode = (SLNode*)malloc(sizeof(Datatype));if (newnode == NULL){perror("malloc");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}
这里就迎来了第二个坑, 这段代码哪里有问题?
首先我们要清楚链表开辟空间是给谁开辟的,链表的空间是给整个节点(结构体)开辟的,而不是仅仅给数据开辟空间。
所以说这里malloc的大小应该是结构体的大小,改为
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
就可以啦!
在其他功能实现之前,我们先理解以下打印链表的函数接口。
void SLprint(SLNode* phead)
{SLNode* cur = phead;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}
打印链表首先需要遍历链表,phead为链表的头指针,但是一般情况下我们并不会直接的使用头指针去遍历,因为那样可能会造成头节点的丢失,所以我们需要创建另一个变量来接收头指针去执行遍历操作。我们要想让cur向后移动就只需把下一个节点的地址赋值给cur,我们知道一个链表的节点内会存储着下一个节点的地址,也就是当前节点中的next。注意这里的遍历需要好好的理解,这是进行后续理解的前提。
理解完链表的的遍历,接下我们来说说它们是如何将每个新建的节点链接的。初学链表可能会有这样的疑惑:创建的节点是如何一个个链接起来的呢?节点的链接是属于后续头插,尾插等操作的内容,但是为了解答大家内心的疑惑,我们先写一个简单的测试接口,来演示它是如何链接的
void test1()
{SLNode* plist = NULL;//链表的头指针,没有节点的情况下指向NULLint n = 0;printf("请输入链表的长度\n");scanf("%d", &n);printf("请输入数据\n");for (int i = 0; i < n; i++){int val = 0;scanf("%d", &val);SLNode *newnode= NewNode(val);//创建结构体指针变量来接收新节点的地址//头插的方式链接newnode->next = plist;plist = newnode;}SLprint(plist);
}int main()
{test1();return 0;
}
首先我们把头指针指向的内容(可能是NULL,也可能是第一个节点)赋值给新节点。

初始情况下,plist指向NULL,把plist指向的内容赋值给新节点newnode的next(指针域),然后把整个新节点的地址赋给plist。

这样plist就成功指向了第一个节点。那么后续插入增加节点也是这样。假设上一步链接的节点地址是0x0012ff40。

把plist指向的节点(地址)赋值给新建节点newnode的next(结构体内的成员)。

然后把整个新节点的地址赋给plist。这样就通过头插将节点链接了。但是这里需要注意,测试接口的操作是在同一个函数内进行创建头指针,节点链接操作的(不需要传值),没有调用函数进行头插链接(在后续实现头插,尾插接口的过程中需要使用二级指针传参进行操作)。
接着我们继续,尾插操作的实现。
尾插的原理是什么?找到最后一节点。

将新节点的地址赋给最后一个节点的next(指针域)就可以了。

注意:新节点的next(指针域)在创建初始化时就已经置为NULL。
但这并不完整,前边我们提到,尾插也可以用来初始化,那么对于没有节点的情况,上述逻辑就无法使用了。所以我们需要特殊处理一下,判断链表是否为NULL。
void SLPushBlack(SLNode* phead, Datatype x)
{SLNode* newnode = NewNode(x);SLNode* tail = *pphead;if (phead == NULL){phead = newnode;}else{while (tail->next)//找到最后一个节点{tail = tail->next;}tail->next = newnode;}
}
以上的逻辑可以这样实现,那段代码是否正确呢?这里就是遇到的第三个坑。运行测试一下我们会发现链表为空的时候没有插入数据。这是为什么呢?
那是因为传进来的头节点是形参,形参是实参的临时拷贝,出了函数phead就被销毁了,在函数内将新节点地址赋给形参 头节点并不会对实参中的头节点造成影响。那这里要想修改实参中头节点的指向就只能传头节点的地址(二级指针),通过实参中头指针的地址来修改头指针的指向。
所以正确的代码应该是:
void SLPushBlack(SLNode** pphead, Datatype x)
{SLNode* newnode = NewNode(x);SLNode* tail = *pphead;if (*pphead == NULL){*pphead = newnode;}else{while (tail->next){tail = tail->next;}tail->next = newnode;}
}
//调用时要传结构体指针的地址
SLPushBlack(&plist,100);
那或许会有人疑惑,为什么链表不为的时候就不使用二级指针?
这里我们总结一下,使用二级指针是因为要修改结构体指针(头指针)的指向,而链表不为空时,只需要链接增加一个节点就好,这时修改的是结构体成员的内容。
对头插操作进行实现
使用二级指针是因为要修改结构体指针(头指针)的指向,那按照逻辑,头插每次都是修改头指针的指向,所以头插独立实现成一个函数时也需要二级指针,尾插是只有第一次链表为空的时候需要二级指针,而头插次次都需要二级指针。
注意前边测试的接口头插数据没有使用二级指针是因为创建头指针和修改头指针指向在同一个函数中,不存在函数调用头指针。
前边我们已经了解头插的逻辑,这里就不再讲解。代码实现:
void SLPushFront(SLNode** pphead, Datatype x)
{SLNode* newnode = NewNode(x);newnode->next = *pphead;*pphead = newnode;
}
pphead就是指向plist(头指针)的指针,*pphead就等价于plist。通过对pphead解引用找到plist(头指针),直接修改plist(头指针)的指向。
总结
本期内容主要介绍在刚开始学习链表时需要注意的点、涉及的基础知识以及逻辑基础,一定要理解透彻,否则后续的接口实现将会寸步难行,好的,本期内容到此就要结束啦,希望对你有所帮助,最后,感谢阅读!
相关文章:
数据结构入门指南:链表(新手避坑指南)
目录 前言 1.链表 1.1链表的概念 1.2链表的分类 1.2.1单向或双向 1.2.2.带头或者不带头 1.2.33. 循环或者非循环 1.3链表的实现 定义链表 总结 前言 前边我们学习了顺序表,顺序表是数据结构中最简单的一种线性数据结构,今天我们来学习链表&#x…...
SpringBoot第24讲:SpringBoot集成MySQL - MyBatis XML方式
SpringBoot第24讲:SpringBoot集成MySQL - MyBatis XML方式 上文介绍了用JPA方式的集成MySQL数据库,JPA方式在中国以外地区开发而言基本是标配,在国内MyBatis及其延伸框架较为主流。本文是SpringBoot第24讲,主要介绍MyBatis技栈的演…...
linux 查看网卡,网络情况
1,使用nload命令查看 #yum -y install nload 2, 查看eth0网卡网络情况 #nload eth0 Incoming也就是进入网卡的流量,Outgoing,也就是从这块网卡出去的流量,每一部分都有下面几个。 – Curr:当前流量 – Avg…...
在Mac上搭建Gradle环境
在Mac上搭建Gradle环境: 步骤1:下载并安装Java开发工具包(JDK) Gradle运行需要Java开发工具包(JDK)。您可以从Oracle官网下载适合您的操作系统版本的JDK。请按照以下步骤进行操作: 打开浏览器…...
Docker网络与Docker Compose服务编排
docker网络 docker是以镜像一层一层构建的,而基础镜像是linux内核,因此docker之间也需要通讯,那么就需要有自己的网络。就像windows都有自己的内网地址一样,每个docker容器也是有自己的私有地址的。 docker inspect [docker_ID]…...
opencv+ffmpeg环境(ubuntu)搭建全面详解
一.先讲讲opencv和ffmpeg之间的关系 1.1它们之间的联系 我们知道opencv主要是用来做图像处理的,但也包含视频解码的功能,而在视频解码部分的功能opencv是使用了ffmpeg。所以它们都是可以处理图像和视频的编解码,我个人感觉两个的侧重点不一…...
开发基于 LoRaWAN 的设备须知--最大兼容性
最大兼容性配置简介 LoRaWAN开放协议的建立前提是每个制造的设备都可以被唯一且安全地识别。配置是创建唯一标识和相应秘密的过程。虽然配置过程是常规的,但存在一些可能并不明显的陷阱。本章尝试描述配置基于 LoRa 的设备的一些最佳实践。 配置概念 基于 LoRa 的设备配置与银…...
一、SpringBoot基础[日志]
一、日志 解释:SpringBoot使用logback作为默认的日志框架,其中还可以导入log4j2等优秀的日志框架 1.修改日志内容 修改整个日志格式:logging.pattern.console%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{15} 你好 %msg%n %d{yyy…...
libuv库学习笔记-networking
Networking 在 libuv 中,网络编程与直接使用 BSD socket 区别不大,有些地方还更简单,概念保持不变的同时,libuv 上所有接口都是非阻塞的。它还提供了很多工具函数,抽象了恼人、啰嗦的底层任务,如使用 BSD …...
C++多线程编程(第三章 案例1,使用互斥锁+ list模拟线程通信)
主线程和子线程进行list通信,要用到互斥锁,避免同时操作 1、封装线程基类XThread控制线程启动和停止; 2、模拟消息服务器线程,接收字符串消息,并模拟处理; 3、通过Unique_lock和mutex互斥方位list 消息队列…...
IOS UICollectionView 设置cell大小不生效问题
代码设置flowLayout.itemSize 单元格并没有改变布局大小, 解决办法如下图:把View flow layout 的estimate size 设置为None,上面设置的itemSize 生效了。...
浅谈3D隐式表示(SDF,Occupancy field,NeRF)
本篇文章介绍了符号距离函数Signed Distance Funciton(SDF),占用场Occupancy Field,神经辐射场Neural Radiance Field(NeRF)的概念、联系与区别。 显式表示与隐式表示 三维空间的表示形式可以分为显式和隐式。 比较常用的显式表…...
软件测试技能大赛任务二单元测试试题
任务二 单元测试 执行代码测试 本部分按照要求,执行单元测试,编写java应用程序,按照要求的覆盖方法设计测试数据,使用JUnit框架编写测试类对程序代码进行测试,对测试执行结果进行截图,将相关代码和相关截…...
MybatisPlus拓展篇
文章目录 逻辑删除通用枚举字段类型处理器自动填充功能防全表更新与删除插件MybatisX快速开发插件插件安装逆向工程常见需求代码生成 乐观锁问题引入乐观锁的使用效果测试 代码生成器执行SQL分析打印多数据源 逻辑删除 逻辑删除的操作就是增加一个字段表示这个数据的状态&…...
设置k8s中节点node的ROLES值,K8S集群怎么修改node1的集群ROLES
设置k8s中节点node的ROLES值 1.查看集群 [rootk8s-master ~]# kubectl get nodes NAME STATUS ROLES AGE VERSION k8s-master Ready control-plane,master 54d v1.23.8 k8s-node1 Ready <none> 54d v1.…...
【*1900 图论】CF1328 E
Problem - E - Codeforces 题意: 思路: 注意到题目的性质:满足条件的路径个数是极少的,因为每个点离路径的距离<1 先考虑一条链,那么直接就选最深那个点作为端点即可 为什么,因为我们需要遍历所有点…...
微信开发者工具 miniprogram_npm 未找到
背景 微信开发者工具中,打开集成了vant-weapp的项目,构建npm时,报错\miniprogram_npm\ 未找到。 问题 微信开发者工具,工具----->构建npm时,提示 message:发生错误 Error: D:\some\path\miniprogram…...
计算机视觉(三)未有深度学习之前
文章目录 图像分割基于阈值、基于边缘基于区域、基于图论 人脸检测Haar-like特征级联分类器 行人检测HOGSVMDPM 图像分割 把图像划分成若干互不相交的区域。经典的数字图像分割算法一般是基于灰度值的两个基本特征之一:不连续性和相似性。 基于阈值、基于边缘 基于…...
二十六、媒体查询2
目录: 媒体查询介绍网页常用分界点 一、媒体查询介绍 媒体特性: width 视口的宽度 height 视口的高度 一般设计的时候,高度不考虑,只考虑宽度 //当视口的宽度是500像素的时候,变颜色media (width: 500px) {body{background-colo…...
Themis 国库建设计划启动,开启去中心化新征程
在未来的金融领域,去中心化金融(DeFi)正在成为一种重要的趋势。在这股DeFi热潮中,作为Filecoin 生态下的一颗璀璨明珠,Themis 上线仅2个月,多项数据便稳居Filecoin-FVM榜首,TVL更是牢牢处于File…...
基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云
目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...
【HTTP三个基础问题】
面试官您好!HTTP是超文本传输协议,是互联网上客户端和服务器之间传输超文本数据(比如文字、图片、音频、视频等)的核心协议,当前互联网应用最广泛的版本是HTTP1.1,它基于经典的C/S模型,也就是客…...
LRU 缓存机制详解与实现(Java版) + 力扣解决
📌 LRU 缓存机制详解与实现(Java版) 一、📖 问题背景 在日常开发中,我们经常会使用 缓存(Cache) 来提升性能。但由于内存有限,缓存不可能无限增长,于是需要策略决定&am…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
在Spring Boot中集成RabbitMQ的完整指南
前言 在现代微服务架构中,消息队列(Message Queue)是实现异步通信、解耦系统组件的重要工具。RabbitMQ 是一个流行的消息中间件,支持多种消息协议,具有高可靠性和可扩展性。 本博客将详细介绍如何在 Spring Boot 项目…...
