当前位置: 首页 > news >正文

数据结构初阶-单链表

        链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

        而我们主要要熟悉的单链表与双向链表的全称分别为:不带头单向不循环链表,带头双向循环链表,当我们对这两种链表熟悉后,便能不费力的推出另外四种链表。我们本文主要介绍单链表。

 一,单链表的概念与结构

1.1概念       

         概念:链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

        这里我们可以联想平时我们所坐的高铁或火车,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉/ 加上,不会影响其他车厢,每节车厢都是独立存在的。

        那,在链表中我们的火车车厢又是怎么样的:

 1.2结点

        与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为“结点/结点”。结点的组成主要有两个部分:当前结点要保存的数据和保存下⼀个结点的地址(指针变量)。

        图中指针变量 plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望 plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x00B0。

1.3链表的性质

1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续

2、结点⼀般是从堆上申请的

3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

        结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码:(因为我们存储的数据类型不定,所以我们可以先将我们要存储的数据类型先定义下):

typedef int SLDataType;typedef struct SLTistNode
{SLDataType Data;struct SLTistNode* next;
}SLTNode,*PSLTNode;

        当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。

接下来让我们来实现单链表的常用功能,实现的功能大多与顺序表类似:

二,单链表功能的实现

2.1链表的初始化

        在介绍它的初始化之前,我们先来介绍什么是带头(哨兵位)和不带头,对于带头,则是链表头部初始时便有一个哨兵位节点,但不存放任何数据,由于我们的单链表为不带头链表,所以我们这里初始化直接引用上面的结构体,设置我们的链表头为空指针即可:

PSLTNode plist = NULL;

2.2获取新节点函数SLTBuyNode

       在对链表进行头尾插之前,由于我们每次插入节点都需要创建新的节点,所以我们则合理直接分装一个函数,后面实现头尾插功能直接调用即可,老样子,我们先给出代码:

PSLTNode SLTBuyNode(SLDataType x)
{PSLTNode node = (PSLTNode)malloc(sizeof(SLTNode));if (node == NULL){perror("newnode:");exit(1);}node->Data = x;node->next = NULL;return node;
}

       由于我们的链表内存的大小是动态变化的,所以我们需要使用malloc函数来开辟节点的空间。为了确保我们代码的健壮性,我们需要检验空间开辟是否成功。然后将所需要插入的数据赋值给新节点的data,同时将新节点的next置为空。

2.3尾插函数SLTPushBack

       由于我们这里是在链表的尾端进行尾插,所以我们直接将当前的链表尾节点的指针设置为我们要创建的新节点的地址:

void SLTPushBack(PSLTNode* pphead, SLDataType x)
{assert(pphead);PSLTNode newcode = SLTBuyNode(x);if (*pphead == NULL){*pphead = newcode;}else{PSLTNode pcur = *pphead;while (pcur->next){pcur = pcur->next;}pcur->next = newcode;}
}

        由于我们初次插入节点时,链表此时为空,所以我们在其为空时直接将指向新节点的指针设置为指向链表头节点的指针,但尾插我们需要遍历链表找到尾节点,所以时间复杂度为O(N)。

2.4头插函数SLTPushFront

       相对于尾插法,我们的头插法就简单许多,只需要将要插入的新节点的next指针直接设置为指向链表头节点的指针,然后再将头节点的指针设置为指向新节点的指针即可:

void SLTPushFront(PSLTNode* pphead, SLDataType x)
{assert(pphead);PSLTNode newcode = SLTBuyNode(x);if (*pphead == NULL){*pphead = newcode;}else{newcode->next = *pphead;*pphead = newcode;}
}

  我们可以明显的看到,头插法的方式没有遍历数组,它的时间复杂度为O(1)。

2.5尾删函数SLTPopBack 

       在进行尾删之前,我们需要判断链表中是否还有节点,如果没有节点,我们就不能再对链表进行删除:

void SLTPopBack(PSLTNode* pphead)
{assert(pphead && *pphead);PSLTNode pcur = *pphead;PSLTNode pcur1 = NULL;while (pcur->next){pcur1 = pcur;pcur = pcur->next;}if (pcur1){pcur1->next = NULL;free(pcur);pcur = NULL;}else{free(*pphead);*pphead = NULL;}
}

        首先我们要遍历链表找到尾节点,我们可以先对头节点的NEXT判空,如果为空,则说明链表中的节点只有一个,我们直接释放头节点即可。如果不为空,我们则直接循环找到尾节点,对其进行释放,同时将其上一个节点的NEXT置为空。

2.6头删函数SLTPopFront

         老样子,我们需要先判断链表是否还有节点,有直接先记住头节点的指针,然后让头指针指向它的NEXT即可:

void SLTPopFront(PSLTNode* pphead)
{assert(pphead && *pphead);PSLTNode pcur = *pphead;*pphead = pcur->next;free(pcur);pcur = NULL;
}

       我们在这里会发现,头删相比于尾删也简单很多,所以当我们在没有特殊要求的情况下,尽量使用头删/插对链表进行增删操作,这样复杂度会少很多。

2.7寻找指定节点函数SLTFind

       在实现删除/增加指定位置数据之前,由于这两个函数都需要寻找数据,所以我们可以直接将寻找函数分装为一个函数,便于我们后面直接调用:

PSLTNode SLTFind(PSLTNode phead, SLDataType x)
{assert(phead);PSLTNode pcur = phead;while (pcur){if (pcur->Data == x){return pcur;}pcur = pcur->next;}return NULL;
}

       如果没有找到目标数据,我们直接返回空指针即可,同时我们需要遍历链表一个一个去寻找,这是我们常用的寻找方法。

2.8向指定位置之后插入数据函数SLTInsertAfter

       我们这里不介绍在之前插入数据的原因是因为此时我们需要遍历链表去找到指定位置前方的数据,所以复杂度会高很多,而这里我们删除指定位置之后则不需要遍历链表:

void SLTInsertAfter(PSLTNode pos, SLDataType x)
{assert(pos);PSLTNode newcode = SLTBuyNode(x);newcode->next = pos->next;pos->next = newcode;
}

需要注意的即是对NEXT指针指向的改变,实现难度不高。

2.9删除指定位置之后的数据函数SLTEraseAfter

 与之前的删除一样,我们需要先判断链表中是否有节点:

void SLTEraseAfter(PSLTNode pos)
{assert(pos && pos->next);PSLTNode del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}

  会了我们上面的头删与尾删函数之后,其实这部分很简单。

2.10链表的销毁函数SListDestroy 

void SListDestroy(PSLTNode* pphead)
{assert(pphead && *pphead);PSLTNode pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}

这里我们也需要判断链表是否为空,如果为空我们也就没必要释放链表空间 。

三,几道单链表OJ题推荐

203. 移除链表元素 - 力扣(LeetCode)

206. 反转链表 - 力扣(LeetCode)

876. 链表的中间结点 - 力扣(LeetCode)

21. 合并两个有序链表 - 力扣(LeetCode)

链表分割_牛客题霸_牛客网 (nowcoder.com)

链表的回文结构_牛客题霸_牛客网 (nowcoder.com)

160. 相交链表 - 力扣(LeetCode)

141. 环形链表 - 力扣(LeetCode)

142. 环形链表 II - 力扣(LeetCode)

       这里尤其是最后两道,使用了Floyd龟兔赛跑算法,非常推荐去练习学习 。

       

 

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Mysql随记
  • 阿里云OSS对象存储的项目实战操作
  • uniapp中给data中的变量赋值报错
  • matlab仿真 模拟调制(下)
  • 总结——TI_音频信号分析仪
  • Golang | Leetcode Golang题解之第260题只出现一次的数字III
  • Llama 3.1要来啦?!测试性能战胜GPT-4o
  • Docker+consul容器服务的更新与发现
  • Ubuntu 22.04安装Visual Studio Code(VS Code)配置C++,Python
  • 【故障排除】Unity在编辑器模式下Play时闪退
  • C++STL详解(一)——string类的接口详解(下)
  • 北醒单点激光雷达更改id和波特率以及Ubuntu20.04下CAN驱动
  • SQL中的函数
  • [k8s源码]7.indexer
  • 设计模式13-单件模式
  • JS题目及答案整理
  • MySQL几个简单SQL的优化
  • Python3爬取英雄联盟英雄皮肤大图
  • React-flux杂记
  • spring security oauth2 password授权模式
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • 从tcpdump抓包看TCP/IP协议
  • 缓存与缓冲
  • 前端技术周刊 2019-02-11 Serverless
  • 听说你叫Java(二)–Servlet请求
  • 详解移动APP与web APP的区别
  • 项目实战-Api的解决方案
  • 小程序上传图片到七牛云(支持多张上传,预览,删除)
  • 优秀架构师必须掌握的架构思维
  • Spring Batch JSON 支持
  • ​香农与信息论三大定律
  • #include<初见C语言之指针(5)>
  • #免费 苹果M系芯片Macbook电脑MacOS使用Bash脚本写入(读写)NTFS硬盘教程
  • (MATLAB)第五章-矩阵运算
  • (Windows环境)FFMPEG编译,包含编译x264以及x265
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (求助)用傲游上csdn博客时标签栏和网址栏一直显示袁萌 的头像
  • (数位dp) 算法竞赛入门到进阶 书本题集
  • (贪心 + 双指针) LeetCode 455. 分发饼干
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • (转)visual stdio 书签功能介绍
  • .NET MVC之AOP
  • .NET 反射的使用
  • .NET/C# 避免调试器不小心提前计算本应延迟计算的值
  • .php文件都打不开,打不开php文件怎么办
  • /etc/X11/xorg.conf 文件被误改后进不了图形化界面
  • @Bean有哪些属性
  • @PostConstruct 注解的方法用于资源的初始化
  • @PreAuthorize与@Secured注解的区别是什么?
  • @vue-office/excel 解决移动端预览excel文件触发软键盘
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798
  • [4]CUDA中的向量计算与并行通信模式
  • [AI 大模型] Meta LLaMA-2
  • [AIGC 大数据基础]hive浅谈
  • [Angular] 笔记 6:ngStyle