【数据结构与算法】数据结构初阶:详解顺序表和链表(三)——单链表(上)
🔥个人主页:艾莉丝努力练剑
❄专栏传送门:《C语言》、《数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题
🍉学习方向:C/C++方向
⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平
前言:本篇文章,我们复盘顺序表和链表相关的知识点,在初阶的数据结构与算法阶段,我们把知识点分成三部分,复杂度作为第一部分,顺序表和链表、栈和队列、二叉树为第二部分,排序为第二部分,我们之前已经介绍完了第一部分:算法复杂度,本文我们继续学习第二部分中的顺序表和链表部分内容啦。
半个多月前,博主更新了头插、尾删、头删、随机位置插入、随机位置删除、查找、修改、菜单等内容,本篇文章,我们就来复盘一下动态顺序表的内容,博主会添加很多新内容,希望对大家的顺序表学习有所帮助。
目录
正文
三、单链表
(一)准备工作
(二)实现单链表
1、打印链表
2、关于初始化
结尾
正文
三、单链表
(一)准备工作
前面我们实现了顺序表的数据结构,顺序表的短板有这些:
1、中间/头部的插⼊删除,时间复杂度为O(N)。
2、增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3、增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
有没有这样一种数据结构,可以做到以下几点:
1、头部插入删除,时间复杂度O(1);
2、不需要增容;
3、不存在空间浪费。
既然话都说到这份儿上了,肯定是存在这种数据结构的嘛。
没错,今天的主角——链表——就粉墨登场了。
为什么大标题取得是《单链表》嘞?这里我们简单介绍一下——
链表是一种数据结构,分成很多形态,单链表属于线性表的一种,所以从这句话我们就可以知道,单链表的逻辑结构是线性的,在物理结构上不一定是线性的。
我们来看一下链表的概念和结构——
概念:
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
刚才那句话也可以从这个概念里面得出来——
逻辑结构:是线性的;
物理结构:不一定是线性的。
我们严谨一点。
单链表的功能:
与前面介绍过的顺序表相似,单链表也能实现增删查改这几个功能。
单链表究竟长什么样子?大家小时候有没有玩过那种一节一节的火车玩具,一买就是一大箱,里面还有小房子、轨道、信号灯、草木什么的,如果没有的话我们就想象一下火车,或者藕(藕也是恰当,一节一节,有节点)、结绳什么的,我们这里就以火车为比喻介绍链表的结构吧。
我们知道,火车在淡季的时候车次的车箱会相应减少,旺季的时候车次的车厢会额外增加几节(比如春运高峰期,候补之所以能补上就是因为铁路部门看人数较多就加车厢了),火车的车厢与车厢之间就是用“挂钩”勾住的,我们讲到车厢时用的量词也是“一节车厢”。
火车车厢其实是独立的,不是强粘连的关系,每个车厢都是独立的,你给每节车厢取个编号,这些编号也不会影响先后的顺序,你只要要用的时候连起来,不用的时候把“挂钩”取下来就行了。
链表也是这样的结构。那么在链表里面,每节“车厢”是什么样的呢?
链表有一个一个节点组成,而这些节点有两个组成部分:当前节点要保存的数据和保存下一个节点的地址(指针变量),即
1、 保存的数据;
2、指针:始终保存下一个节点的地址。
链表为空:
int* olist = NULL;
我们在画图班上抽象出来的是线性的结构:
指针变量plist保存的是第一个结点的地址,我们称plist此时“指向”第一个结点,如果我们希望 plist“指向”第二个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独立申请的(即需要插⼊数据时才去申请一块结点的空间),我们需要通过指针变量来保存下一个结点位置才能从当前结点找到下一个结点。
但实际上在内存里面它们都是相互独立的,东一块西一块儿,地址不是连续的:
内存中,这些节点的存放是在堆区——
这样相互独立、地址也不连续的几个节点是怎么连在一起的?这时,节点保存的下一个节点的地址就发挥作用了。虽然每个节点都是独立的,但是我们能通过下一个节点的地址找到下一个节点:
这个箭头实际上是不存在的,为了方便我们理解加上的箭头,每个节点都是独立的。
我们定义链表的数据结构其实就是直接定义节点的结构就可以了。
C语言中我们介绍了结构体,我们这样写——
struct ListNode
{...
}
光写ListNode不行,得加上一个“s”,这个“s”就是single(单个的)。
struct SListNode
{int data;...*next;
}
这里我们先把存储的数据(假设是整型)写好,接下来第二个部分,它是一个指针,我们就叫它“next”,这个*next的数据类型是什么?
数据类型是SListNode,但我们这里写需要加上struct,因为我们这里还没有typedef重命名:
struct SListNode
{int data;struct SListNode* next;
}
我们让Head指向头节点,一会儿还有个tail指向尾节点,很形象吧。如下图——
做完这些我们就可以开始写代码了。
(二)实现单链表
本文实现单链表用的是C语言(因为博主还没有怎么学过C++……)。
和顺序表一样,我们创建好三个文件,先把肯定要用到的几个头文件写好:
我们节点要申请空间吧,把stdlib.h带上,其他的头文件我们暂时先不管,先定义链表的结构:
这个Node就是节点,我们定义链表的结构其实就是定义节点的结构。
我们定义一下数据的类型,把它替换掉——
我们在使用这个结构体的时候每次都要加上关键词struct,非常麻烦。我们也可以给当前的结构体取一个别名,有两种方式,任选一种即可——
方式(1):
方式(2):
这里再考考大家,我们可以这样写吗?
很明显,报错了,为什么不能这样写呢?因为C语言编译器是向上编译的,当我们在这里写SLTNode的时候,它会向上去找SLTNode,找不到的,只能找到struct SListNode,所以我们这里可不能写成SLTNode哦!
接下来,我们在test.c测试文件中创建一下链表——
说是创建一个链表——实际上是创建一个一个节点,再把节点连起来——
#include"SList.h"int test01()
{//创建一个链表——实际上是创建一个一个节点,再把节点连起来SLTNode*node1=(SLTNode*)malloc(sizeof(SLTNode));SLTNode*node2=(SLTNode*)malloc(sizeof(SLTNode));SLTNode*node3=(SLTNode*)malloc(sizeof(SLTNode));SLTNode*node4=(SLTNode*)malloc(sizeof(SLTNode));
}
当我们写完这样一组代码之后,我们的链表就创建完了——
现在我们要打印链表,我们在头文件里面定义一个打印链表的方法,这个方法我们就在SList.c文件里面去实现——
我们可以用phead表示第一个节点——
这个代码看不懂没关系,接下来我们来实现一下打印链表。
1、打印链表
此时三个文件的代码如下:
(1)SList.h
#pragma once#include<stdio.h>
#include<stdlib.h>//链表的结构
typedef int SLTDatatype;
typedef struct SListNode
{SLTDatatype data;struct SListNode* next;//指向下一个节点的地址
}SLTNode;//typedef struct SListNode SLTNode;void SLTPrint(SLTNode* phead);
(2)SList.c
#define _CRT_SECURE_NO_WARNINGS 1#include"SList.h"void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur != NULL){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
(3)test.c
#define _CRT_SECURE_NO_WARNINGS 1#include"SList.h"int test01()
{//创建一个链表——实际上是创建一个一个节点,再把节点连起来SLTNode*node1=(SLTNode*)malloc(sizeof(SLTNode));SLTNode*node2=(SLTNode*)malloc(sizeof(SLTNode));SLTNode*node3=(SLTNode*)malloc(sizeof(SLTNode));SLTNode*node4=(SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;node2->data = 2;node3->data = 3;node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;SLTNode* plist = node1;//打印链表SLTPrint(plist);
}int main()
{test01();return 0;
}
这代码运行的结果和前面的这张图一模一样——
不信我们试一试——
这里链表是怎么打印的如果搞清楚了,等会儿我们写各种方法的代码就简单了。
为什么我们在这里虽然没有让它循环往下执行,程序却没有死循环,关键在这个语句——
这是一个赋值语句,pcur = pcur->next保存的地址,赋值语句是从右往左走,所以我们先看
pcur->next保存的地址,pcur->next是什么?其实就是当前节点next指针存储的数据。
plist指向头节点, 我们已经规定phead是第一个节点,这里pcur指向的是第一个节点,我们把第二个节点的地址给了pcur,此时,pcur指向的就是第二个节点。
pcur走到第二个节点之后,继续循环,pcur不为空,打印当前指针中的值(这里是2),打印之后再把pucr下一个节点存放的地址给pcur,当前pcur存的地址是:
我们要把下一个节点的地址给pcur,pcur又从第二个节点走到了第三个节点——
打印完我们再把下一个节点的地址给pcur,为空吗?不为空,打印当前节点的值(这里是3)。
第四个节点为空吗?不为空,打印当前节点里的值4,打印完之后,继续把pcur->next指针存储的地址给pcur,此时pcur是NULL了,它不再指向第四个节点,继续循环,pcur为NULL,跳出循环,打印了NULL和换行符,这就是链表打印的操作。
虽然不像数组可以+i解引用的方式直接访问到下个节点的值,但是在链表中由于节点与节点之间是不连续的,我们想要从第一个节点遍历到第二个节点,通过当前节点的next指针走到下一个节点。
2、关于初始化
顺序表中我们讲了顺序表的初始化。那么在链表中我们需不需要初始化呢?不需要初始化。
对于顺序表没有数据或者说数据为空的情况下,要么底层的数组没有空间,我们把它置为NULL,要么有数据我们需要提前给它申请空间,size和capacity也要置为初始值。
链表始终有一个plist指针指向第一个节点,
SLTNode* plist = NULL;
所以不存在链表的初始化,初始情况下,如果链表里面没有数据,那这个链表肯定为空。
接下来我们肯定要对链表插入数据,它才可能变成非空。
我们已经有了链表这个结构,那么接下来我们就要来看链表里面如何插入数据了。
结尾
单链表的剩余部分,像增删查改等功能的实现、完整的代码过程由于篇幅受限,博主都放在下篇了,下篇会在明天更新,会在下篇附上【单链表(上)】的链接。
往期回顾:
【数据结构与算法】数据结构初阶:动态顺序表各种方法(接口函数)复盘与整理
【数据结构与算法】数据结构初阶:详解顺序表和链表(二)
【数据结构与算法】数据结构初阶:详解顺序表和链表(一)
【数据结构】详解算法复杂度:时间复杂度和空间复杂度
本期内容需要回顾的C语言知识如下面的截图中所示(指针博主写了6篇,列出来有水字数嫌疑了,就只放指针第六篇的网址,博主在指针(六)把指针部分的前五篇的网址都放在【往期回顾】了,点击【传送门】就可以看了),大家如果对前面部分的知识点印象不深,可以去上一篇文章的结尾部分看看,上一篇文章的链接博主放在下面了:
【数据结构与算法】数据结构初阶:动态顺序表各种方法(接口函数)复盘与整理
结语:本篇文章到这里就结束了,对数据结构的单链表知识感兴趣的友友们可以在评论区留言,博主创作时可能存在笔误,或者知识点不严谨的地方,大家多担待,如果大家在阅读的时候发现了行文有什么错误欢迎在评论区斧正,再次感谢友友们的关注和支持!