一篇博客读懂单链表——Single-List

目录

一、初识单链表

单链表是如何构造的:

单链表如何解决顺序表中的问题:

二、单链表的初始定义 

三、尾插和头插

3.1 新建结点CreateNode

3.2 打印SLTPrint

3.3 尾插SLTPushBack 

3.4 头插SLTPushFront 

四、尾删和头删

4.1 尾删SLTPopBack

4.2 头删SLTPopFront

五、某位置前插入删除

5.1 查找SLTFind

5.2 某位置前插入SLTInsert

5.3 某位置删除SLTErase

六、某位置后插入删除

七、 assert断言 

7.1 SLTPrint断言

7.2 SLTPushBack 和 SLTPushFront 断言

7.3 SLTPopBack 和 SLTPopFront 断言

7.4 SLTInsert 和 SLTErase 断言


顺序表的问题:

1.尾部插入效率还不错,头部或者中间插入删除,需要挪动数据,效率低下

2. size 满了后只能扩容,扩容是有一定消耗的;扩容一般存在一定的空间浪费

我们总结了顺序表的问题,下面我们学习的单链表就可以让这些问题迎刃而解。 

学习要求:掌握C语言的指针、二级指针、动态开辟内存的知识

一、初识单链表

我们首先来回顾一下顺序表中的结构体成员:

顺序表中单个结构体存储着顺序表的表头指针、整个顺序表的数据容量、实际存储的数据容量......

单链表是如何构造的:

链表每个结点会存储一个数据,同时会存储一个指针,这个指针指向下一个结点。

单链表如何解决顺序表中的问题:

链表的每一个结点都是 malloc 出来的,他们的地址并不是连续分配的,如果从头部或者中间插入删除,只需要改动单个结点,无需挪动整个 table ;因为链表的结点是一个一个开辟的,所以也不存在顺序表的空间浪费。

下面我们来看一下链表实际的结构:

在这里我们强调不需要太过于关注链表的物理结构,我们的注意点应该集中在其是个结构体。

二、单链表的初始定义 

这里还是和顺序表一样的套路,把它当成一个工程去做,当然就要分文件啦。

其次,在 .h 文件中我们要做我们的准备工作:

 接下来我们来看一下我们需要对链表做的操作:

三、尾插和头插

在学习尾插之前,我们需要先思考一个问题,为什么在我们的操作中有时候需要传指针而有时候需要传二级指针呢(下面以尾插为例)?

如果要探索这个问题的答案,我们一定要知道我们在尾插时是需要改变指针的值的。如果我们调用函数需要改变 int 的值,我们是不是需要传 int* 来改变我们的实参,而在这里我们需要改变的是SLTDataType* 的值,所以就需要我们传入指针的地址来对指针的值进行修改。

3.1 新建结点CreateNode

 我们在这还需要写一个函数,这个函数可以说是链表插入元素的精髓了,我们在前面了解到了链表插入元素是一个结点一个结点地 malloc 出来的,所以我们在这里写一个 CreateNode 函数来创建我们的新结点,可以极大地减少我们下面的代码量。

3.2 打印SLTPrint

为了方便后续的检测,我们也要先把显示链表内容的函数定义出来:

3.3 尾插SLTPushBack 

3.4 头插SLTPushFront 

这里要注意的是我们先让 NewNode 作为头结点来指向 *pphead (即phead) 以此来找到之前第一个结点的地址,然后我们再将 *pphead 指向 NewNode,在以后遇到需要头插的题目,我们也要使用这种顺序。

四、尾删和头删

4.1 尾删SLTPopBack

尾删肯定要找到最后一个元素,让指向最后一个元素的指针直接指向 NULL ,然后再 free 掉最后一个结点,再查找会后一个结点时,我们需要改变的是指向它地址的指针的值,所以我们就要用 tail->next->next != NULL 作为寻找条件:

欸,我们从图中和代码中不难发现,当 tail 的后两个元素为 NULL 时才能进行判断,那么如果链表中只有一个结点(tail->next == NULL),这时候应该怎么办呢?当然是单独进行判断啦!

下面我们来看一下完整代码:

但是需要注意的是,如果我们在一开始就定义了 tail 指针指向 *pphead ,当 tail == NULL 时, free 掉 tail ,这样的写法在我们删掉最后一个结点时会在 Print() 函数中报错,错误代码如下:

这是为什么呢?原因是如果提前用 tail 接收 *pphead,那么在 free(tail) 后也应该将 *pphead 置为空,如果仅仅将 tail 置空,那我们的 *pphead 就成了野指针,这样当然是不可取的。

4.2 头删SLTPopFront

头删就相对简单很多,只需要改变 *pphead 的指向,再 free 就好。

 

五、某位置前插入删除

在学习某位置插入删除前,我们需要知道的是我们的“某位置”应该如何定位,比较某个结点与我们的目标值?显然是不行的,当有多个结点的值重复时这个想法自然就会被推翻,那我们应该怎么办呢?我们也要设计一个查找函数,将第一个查找到的结点地址返回,再将该地址传入我们的插入删除函数中。

5.1 查找SLTFind

当我们遍历完整个链表却没找到想要的元素时,说明链表中没有该元素,返回 NULL 。

5.2 某位置前插入SLTInsert

首先我们要判断这里是不是头插,如果是头插,我们直接调用头插函数,如果不是,我们继续我们的移形换影大法,插入结点。 

 

在改变 cur->next 前,我们要用临时变量 tmp 接收 cur 下一个结点的地址,这样才能在后面找到。 

5.3 某位置删除SLTErase

因为我们的 SLTFind 函数可以定位到 val==x 的当前结点,所以我们可以用这一特点删除指定位置的元素。当我们要删除的是第一个结点时,我们同样可以调用头删函数。

 

 

六、某位置后插入删除

这里思路比较简单,我们来简单看一下代码:

七、 assert断言 

最后我们当然不能忘记断言,下面我们一起来看看每个函数中需要的断言:

7.1 SLTPrint断言

SLTPrint不用断言,因为我们已经设置了当链表为空时只需打印 NULL

7.2 SLTPushBack 和 SLTPushFront 断言

这两个函数只需要断言 pphead 这个二级指针,因为我们是允许链表为空时的头插尾插的。

7.3 SLTPopBack 和 SLTPopFront 断言

这两个函数不仅要断言 pphead 还要断言 *pphead ,因为当链表中有结点时才可以Pop

7.4 SLTInsert 和 SLTErase 断言

插入和删除函数不仅需要像上面两个函数一样断言 pphead *pphead ,还要断言 pos

下面是我的单链表源代码库,包含了对每个函数测试用的代码,需要的uu可以自行查看:

手撕单链表 - Gitee.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/170988.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GPT-4 Turbo 发布 | 大模型训练的新时代:超算互联网的调度与调优

★OpenAI;ChatGPT;Sam Altman;Assistance API;GPT4 Turbo;DALL-E 3;多模态交互;算力调度;算力调优;大模型训练;GH200;snowflake;AGI;A…

Git精讲(一)

📘北尘_:个人主页 🌎个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上,不忘来时的初心 文章目录 一、Git初识1、提出问题2、如何解决--版本控制器3、注意事项 二、Git 安装1、Linux-centos2、…

cgo与调用c的回调函数指针

cgo直接调用函数,使用基本数据类型非常简单,包括一些结构体也比较简单,嵌套的稍微复杂些,但也可以,但有的时候,cgo调用c函数,会需要传递一个回调函数的指针,这时候就比较复杂了&…

解释tqdm模块显示进度条:

1. 在Python中&#xff0c;当你使用tqdm模块&#xff08;一个快速、可扩展的Python进度条库&#xff09;时&#xff0c;你可能会看到类似的输出&#xff1a;[6:20:38<6:34:14, 31.25s/it]。 这个输出提供了关于循环进度的详细信息&#xff1a; 6:20:38: 这是已经过去的时…

五分钟利用Vite创建Vue项目

1.准备工具 Vite是尤雨溪团队开发的&#xff0c;官方称是下一代新型前端构建工具&#xff0c;能够显著提升前端开发体验。 上面称是下一代&#xff0c;当前一代当然是我们熟悉的webpack Vite 优势 开发环境中&#xff0c;无需打包操作&#xff0c;可快速的冷启动。轻量快速…

【C++】【Opencv】minMaxLoc()函数详解和示例

minMaxLoc&#xff08;&#xff09;函数 是 OpenCV 库中的一个函数&#xff0c;用于找到一个多维数组中的最小值和最大值&#xff0c;以及它们的位置。这个函数对于处理图像和数组非常有用。本文通过参数和示例详解&#xff0c;帮助大家理解和使用该函数。 参数详解 函数原型…

Flutter有状态组件StatefulWidget生命周期

StatefulWidget是Flutter中的一个有状态的组件&#xff0c;它的生命周期相对复杂一些。下面是StatefulWidget的生命周期方法及其调用顺序&#xff1a; 1. createState(): 当StatefulWidget被插入到Widget树中时&#xff0c;会调用createState()方法来创建与之关联的State对象。…

Redis集群,你真的学会了吗?

目录 1、为什么引入集群 1.1、先来了解集群是什么 1.2、哨兵模式的缺陷 引入集群解决了什么问题 1.3、使用集群&#xff0c;如何存储数据 2、三种主流的分片方式【经典面试题】 2.1、哈希求余算法 2.1.1、哈希求余算法的介绍 2.1.2、哈希求余算法如何扩容 2.2、一致性…

数据分析 - 思考题

上班路上刷到的有趣题

作用域插槽slot-scope

一般用于组件封装&#xff0c;将使用props传入组件的数据再次调出来或者单纯调用组件中的数据。也可用于为组件某个部分自定义样式以及为某次使用组件自定义样式。 直接拿elementui的el-table举例&#xff1a; <template><el-table v-loading"loading&q…

《QT从基础到进阶·十七》QCursor鼠标的不同位置坐标获取

一些常用鼠标图形&#xff1a; 鼠标光标相对于整个电脑屏幕的位置&#xff1a;QCursor::pos() 当前光标相对于当前窗口的位置&#xff1a;this->mapFromGlobal(QCursor::pos()) void MainWindow::mouseReleaseEvent(QMouseEvent* event) {QPoint pos event->pos(); …

设计模式1

![在这里插入图片描述](https://img-blog.csdnimg.cn/c9fbecf1ae89436095885722380ea460.png)一、设计模式分类&#xff1a; 1、创建型模式&#xff1a;创建与使用分离&#xff0c;单例、原型、工厂、抽象、建造者。 2、结构型模式&#xff1a;用于描述如何将对象按某种更大的…