2. 链表

链表的概念及结构

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

链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。

⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾?

最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。

在链表⾥,每节“⻋厢”是什么样的呢?

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点”

节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。

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

为什么还需要指针变量来保存下⼀个节点的位置?

链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。

链表的分类
链表的结构⾮常多样,组合起来有8种(2 x 2 x 2)链表结构:
带头   不带头
单向   双向
循环   不循环
这里的带头不带头说的是头结点  

在链表的数据结构中,头结点(或称为哨兵结点、哑结点)通常被设置在链表的首部,它的作用主要是简化对链表的操作,特别是当链表为空时。头结点不存储有效数据,它的数据域通常不存储任何信息,或者仅用作链表状态的标识。头结点的指针域则指向链表的第一个有效数据结点,即首元结点。

首元结点,也称为第一个结点,是链表中第一个存储有效数据的结点。在带头结点的链表中,首元结点是头结点之后的那个结点;而在不带头结点的链表中,首元结点就是链表的第一个结点。

引入头结点的好处之一是,无论链表是否为空,头指针总是非空的。这简化了对链表是否为空的判断,以及对链表第一个元素的访问。同时,在链表操作(如插入、删除)中,也可以减少特殊情况(如链表为空)的处理,提高代码的健壮性和可读性。

最常用的两种链表是无头单向非循环链表(单链表)和带头双向循环链表
我们会讲解这两种最常见的链表 其他的便不做讲解了

单链表的实现

结合前⾯学到的结构体知识,我们可以给出每个节点对应的结构体代码:

假设当前保存的节点为整型:

当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数

据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。

当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个节点的钥匙)就可以了。

给定的链表结构中,如何实现节点从头到尾的打印?

  1. 结构体指针cur指向第一个结点(保存第一个结点的地址)
  2. 访问结构体成员data
  3. 对cur解引用拿到next指针变量的地址(下一结点的地址)
  4. 如此循环 知道下一结点的地址为NULL

补充说明:

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

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

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

我们上面是输出int类型的数据  

当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?

我们这里只需要对结点数据类型进行重命名即可

typedef  ....  SLTDataType; //只需要输入我们想要的数据类型即可

SList.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//定义节点的结构
//数据+指向下一节点的指针
typedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;void SLTPrint(SLTNode* phead);//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//尾删
void SLTPopBack(SLTNode** pphead);//当尾删  使得*pphead为NULL时  实参被改变//头删
void SLTPopFront(SLTNode** pphead);//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);//销毁链表
void SListDestroy(SLTNode** pphead);

在这里需要注意的是 我们有些操作是需要二级指针的 而有些操作一级指针就可以完成其功能 比如打印 查找 ......

我们在test.c中 会创建指针plist(实参)---指向单链表的第一个结点  当我们要修改第一个结点时  为了让形参的改变影响到实参的改变 我们这里需要传地址 即传&plist 我们既然用了一级指针的地址 那么函数形参需要用二级指针接收

SList.c

#include"SList.h"
//打印
void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);//pphead为空的话 会出现对空指针解引用的问题  但是*pphead可以为空//*pphead 就是指向第一个节点的指针//空链表和非空链表SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL)//判断第一个节点{*pphead = newnode;}else{//找尾SLTNode* ptail = *pphead;//坑1  如果该链表为NULL  会出现对NULL解引用while (ptail->next){ptail = ptail->next;}//ptail指向的就是尾结点ptail->next = newnode;}
}//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//链表不为空  或为空  SLTNode* newnode = SLTBuyNode(x);//*pphead---表示指向第一个节点newnode->next = *pphead;*pphead = newnode;
}void SLTPopBack(SLTNode** pphead)//当尾删  使得*pphead为NULL时  实参被改变
{assert(pphead);//链表不能为空assert(*pphead);//链表只有一个节点if ((*pphead)->next == NULL)//  ->优先级高于*{free(*pphead);*pphead = NULL;}else{//链表中有多个节点SLTNode* prev = *pphead;SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}free(ptail);//当只有一个节点时  会有问题ptail = NULL;prev->next = NULL;}
}//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);//链表不能为空assert(*pphead);SLTNode* next = (*pphead)->next;//想让指针next存储第二个节点的地址free(*pphead);*pphead = next;//让*pphead---一级指针指向next
}//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->data == x){return pcur;}pcur = pcur->next;}//pcur==NULLreturn NULL;//没有找到x
}//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLTBuyNode(x);//pos==*pphead 说明是头插if (pos == *pphead)SLTPushFront(pphead, x);else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}
}//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;
}//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);//pos是头结点   以及pos不是头结点if (pos == *pphead){/*SLTNode* next = (*pphead)->next;free(pos);*pphead = next;*///头删SLTPopFront(pphead);}else {SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//找到prev位置prev->next = pos->next;free(pos);pos = NULL;}
}//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//销毁链表
void SListDestroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}//pcur为空*pphead = NULL;
}

这里我就不给大家test.c代码了  test.c主要是测试我们所写代码能否实现其功能  牢记:每写一个操作 便去测试一下

带头双向循环链表

带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨
的”
“哨兵位”存在的意义: 遍历循环链表避免死循环

双向链表的实现

List.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int LTDataType;
//定义双向链表节点的结构
typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;//声明双向链表中提供的方法//初始化
/*void LTInit(LTNode** pphead);*///让形参的改变影响到实参--传址调用
LTNode* LTInit();//
//初始化结束后 哨兵位结点不能被删除 结点的地址也不能发生改变--所以我们建议传一级指针
//二级指针也行 但我们需要保证哨兵位不被改变
// 
// 销毁
void LTDestroy(LTNode* phead);//尾插
void LTPushBack(LTNode* phead, LTDataType x);//插入数据之前 链表必须初始化到只有一个头结点的情况//打印
void LTPrint(LTNode* phead);//头插
void LTPushFront(LTNode* phead, LTDataType x);//尾删
void LTPopBack(LTNode* phead);//头删
void LTPopFront(LTNode* phead);//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);//删除pos结点
void LTErase(LTNode* pos);LTNode* LTFind(LTNode* phead, LTDataType x);

在写链表代码时 我们一定要搞清楚 每个操作的参数 到底应该是一级指针还是二级指针

在这里 由于我们的头结点(即哨兵位)---区分头结点和首元结点(第一个结点)

在初始化链表时 我们此时是想通过形参的改变影响到实参  理论上初始化操作中应该是二级指针

但在整体代码中 我们使用一级指针更多  为了代码接口统一

List.c

#include"List.h"
//申请结点
LTNode* LTBuyNode(LTDataType x)
{LTNode* node = (LTNode*)malloc(sizeof(LTNode));if (node == NULL){perror("malloc fail!");exit(1);}node->data = x;node->next = node->prev=node;return node;
}
//初始化No.1
//void LTInit(LTNode** pphead)
//{
//	//给双向链表创建一个哨兵位
//	*pphead = LTBuyNode(-1);
//}//No.2
LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;//在test.c中将phead返回给plist即可
}// 销毁  --我们的实参plist最终也需要随phead置为空而为空 按理说要传二级指针
//但是为了保持接口的一致性 才传的一级  
//传一级存在的问题是 当形参phead置为NULL后 plist不会被修改
//这时我们可以采用手动将plist置为NULL来解决该问题
void LTDestroy(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}//此时pcur指向phead 而phead也需要销毁free(phead);phead = NULL;
}//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//为了不影响原链表 我们先动newnodenewnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;//这两行代码不能交换次序
}//打印
void LTPrint(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}//头插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;//d1指向newnodephead->next = newnode;//头结点指向newnode
}//双向链表为空时 此时链表只剩下一个头结点  若头结点为NULL 说明这不是一个有效的双向链表
//尾删
void LTPopBack(LTNode* phead)
{//链表必须有效且链表不能为空(只有一个头结点)assert(phead && phead->next != phead);LTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;//删除del结点free(del);del = NULL;
}//头删
void LTPopFront(LTNode* phead)
{assert(phead && phead->next != phead);LTNode* del = phead->next;phead->next = del->next;del->next->prev = phead;//删除del结点free(del);del = NULL;
}//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x)return pcur;elsepcur = pcur->next;}//没有找到return NULL;
}
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}//删除pos结点 这里pos结点删除并置空为什么不传二级指针呢
//是因为我们前面操作基本都是一级指针 为了保持接口一致性 我们传一级指针
//那我们初始化中 使用了二级指针 能否也改成一级呢 当然可以 
//当然我们这里也可以传二级指针 
//我们函数内让pos置空 但由于形参改变无法使实参改变 所以我们还需要在test.c中 手动让find置为空
void LTErase(LTNode* pos)
{//pos理论上来说不能为phead(哨兵位) 但是没有参数phead 无法增加校验assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}

顺序表和链表(单链表)的优缺点分析

我们链表的知识点也就讲到这里为止  如果大家有对知识点不够清楚的可以评论区d我  有些知识可能会遗漏  

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

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

相关文章

@NameBinding注解名称绑定过滤器/拦截器

NameBinding注解名称绑定过滤器/拦截器&#xff0c;只针对某一些资源方法执行处理逻辑 一、为什么要用名称绑定 一般情况下&#xff0c;借助Spring的过滤器或者拦截器等对Http请求或响应进行处理就能满足需求。但是在有些场景下若只需对特定的xxxResource做拦截处理&#xff0…

【ARM Trace32(劳特巴赫) 使用介绍 12.1 -- Trace32 读写 64位地址】

请阅读【Trace32 ARM 专栏导读】 文章目录 Trace32 读写 64位地址读 64 位地址写64位地址Trace32 读写 64位地址 在使用TRACE32进行调试时,有时需要读取或操作64位的地址,特别是在处理64位的处理器或操作系统时。以下是如何在TRACE32中读取64位地址的一般方法。 读 64 位地…

vue-element-admin vue设置动态路由 刷新页面后出现跳转404页面Bug 解决方法

做项目时遇到的这个bug&#xff0c;因为除了跳404之外也没太大影响&#xff0c;之前就一直放着没管&#xff0c;现在项目基本功能实现了&#xff0c;转头处理了一下&#xff0c;现在在这里记录一下解决方法 这个bug的具体情况是&#xff1a;设置了动态路由之后&#xff0c;不同…

FPGA - ZYNQ 基于Axi_Lite的PS和PL交互

前言 在FPGA - ZYNQ 基于EMIO的PS和PL交互中介绍了ZYNQ 中PS端和PL端交互的开发流程&#xff0c;接下来构建基于基于Axi_Lite的PS和PL交互。 开发流程 Axi_Lite从机 在FPGA - AXI4_Lite&#xff08;实现用户端与axi4_lite之间的交互逻辑&#xff09;中&#xff0c;详解介绍…

微信小程序:基于MySQL+Nodejs的汽车品牌管理系统

各位好&#xff0c;接上期&#xff0c;今天分享一个通过本地MySQLNodejs服务器实现CRUD功能的微信小程序&#xff0c;一起来看看吧~ 干货&#xff01;微信小程序通过NodeJs连接MySQL数据库https://jslhyh32.blog.csdn.net/article/details/137890154?spm1001.2014.3001.5502 …

针对窗口数量多导致窗口大小显示受限制的问题,使用滚动条控制窗口

建议&#xff1a;首先观察结果展示&#xff0c;判断是否可以满足你的需求。 目录 1. 问题分析 2. 解决方案 2.1 界面设计 2.2 生成代码 2.3 源码实现 3. 结果展示 1. 问题分析 项目需要显示的窗口数量颇多&#xff0c;主界面中&#xff0c;如果一次性显示全部窗口&#x…

飞书小技巧:markdown导出

文章目录 下载Feishu2Md飞书应用配置配置feishu2md工具绑定应用导出markdown 下载Feishu2Md Feishu2Md 飞书应用配置 进入飞书开发者后台 https://open.feishu.cn/app。 点击“创建企业自建应用”&#xff0c;并填写应用名称等信息。而后点击创建。 PS: 此处作者创建应用名…

面向对象设计与分析40讲(25)中介模式、代理模式、门面模式、桥接模式、适配器模式

文章目录 门面模式代理模式中介模式 之所以把这几个模式放到一起写&#xff0c;是因为它们的界限比较模糊&#xff0c;结构上没有明显的差别&#xff0c;差别只是语义上。 这几种模式在结构上都类似&#xff1a; 代理将原本A–>C的直接调用变成&#xff1a; A–>B–>…

网渲应用领域有哪些?渲染100邀请码1a12

网渲是一种利用云计算技术把本地渲染上传到云端进行的过程&#xff0c;它极大提高了渲染效率&#xff0c;摆脱了本地限制&#xff0c;使用网渲的领域有很多&#xff0c;这里我们列举下。 1、影视制作 在影视制作当中&#xff0c;对于需要大量特效和动画效果的电影来说&#x…

【STM32】嵌入式实验二 GPIO 实验:数码管

实验内容&#xff1a; 编写程序&#xff0c;在数码管上显示自己的学号。 数码管相关电路&#xff1a; PA7对应的应该是段码&#xff0c;上面的图写错了。 注意&#xff1a;选中数码管是低电平选中&#xff1b;并且用74HC595模块驱动输出的段码&#xff0c; 这个模块的学习可以…

太奇怪了!99%的人没见过的Oracle故障:网络恢复后,集群的监听和vip无法启动

故障描述 15:46操作系统日志出现net4、net5网卡down&#xff0c;15:53分钟的网络恢复。网络中断是由于db汇聚交换机出现了问题。 网络恢复后&#xff0c;节点1的监听和vip无法启动。 故障分析 查看grid alert日志可以看到监听资源确实没有正常启动。 由于监听资源是crs的Ora…

有了可视化工具,你定制设计得瑟瑟发抖了吧,其实你想多了。

目前市面上有N多可视化的工具&#xff0c;可以做成可视化大屏&#xff0c;甚至有很多B端系统也附带可视化页面&#xff0c;据此就有很多人开始怀疑我们这些做定制开发的&#xff0c;还有啥生存空间。 其实你真的多虑了&#xff0c;存在即合理&#xff0c;我们承认可视化工具的标…