数据结构入门指南:链表(新手避坑指南)
目录
前言
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…...

SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)
题目:3442. 奇偶频次间的最大差值 I 思路 :哈希,时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况,哈希表这里用数组即可实现。 C版本: class Solution { public:int maxDifference(string s) {int a[26]…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...

JUC笔记(上)-复习 涉及死锁 volatile synchronized CAS 原子操作
一、上下文切换 即使单核CPU也可以进行多线程执行代码,CPU会给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU会不断地切换线程执行,从而让我们感觉多个线程是同时执行的。时间片一般是十几毫秒(ms)。通过时间片分配算法执行。…...

html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...

OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
Typeerror: cannot read properties of undefined (reading ‘XXX‘)
最近需要在离线机器上运行软件,所以得把软件用docker打包起来,大部分功能都没问题,出了一个奇怪的事情。同样的代码,在本机上用vscode可以运行起来,但是打包之后在docker里出现了问题。使用的是dialog组件,…...

解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...