【数据结构】单链表和双链表

文章目录

  • 一、链表的概念及结构
  • 二、链表的分类
  • 三、无头单向非循环链表
    • 1.单链表创建
    • 2.尾插和头插
    • 3.尾删和头删
    • 4.打印
    • 5.查找
    • 6.插入
    • 7.删除
    • 8.销毁
  • 四、带头双向循环链表
    • 1.双链表的创建
    • 2.初始化
    • 3.判断链表是否为空
    • 4.尾插和头插
    • 5.尾删和头删
    • 6.查找
    • 7.插入
    • 8.删除
    • 9.销毁
  • 五、总结
    • 链表和顺序表的对比

一、链表的概念及结构

通过上篇顺序表我们了解到顺序表是一种顺序存储结构,顺序存储结构是用一段物理地址连续的存储单元依次存储数据元素的线性结构,我们一般采用数组来存储。

而本篇我们介绍的链表是链式存储的结构,链式存储结构的特点是:数据的物理地址是非连续的,可以在内存中的任意位置。也就是说逻辑上相邻的数据元素在空间上不一定连续。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

在这里插入图片描述
因为链表的每个结点是从堆上动态申请的,每次申请的空间是随机的,所以物理地址不一定是连续的。

二、链表的分类

1.带哨兵位头结点或不带头结点

在这里插入图片描述

2.单向或双向

在这里插入图片描述
3.非循环或循环
在这里插入图片描述

一共有2^3 = 8种链表结构。
八种结构中,我们经常用到的只有两种结构:无头单向非循环链表和带头双向循环链表。
接下来我们就主要介绍这两种链表结构。掌握了这两种结构,其他几种也可以自己写出来了。
下面的内容涉及到结构体和指针等相关知识,忘了的小伙伴可以复习一下前面的知识。
传送门:结构体篇,指针篇,动态内存管理

三、无头单向非循环链表

我们所说的单链表通常是指无头单向非循环链表
在这里插入图片描述单链表的定义

typedef int DataType;//int可以换成char、double或者其他结构体类型
typedef struct SingleListNode
{DataType data;//存储数据元素struct SingleListNode* next;//存储下一个结点的地址
}SLNode;

1.单链表创建

//动态申请一个节点
SLNode* BuySLNode(DataType x)
{SLNode* newNode = (SLNode*)malloc(sizeof(SLNode));if (NULL == newNode){perror("malloc");return NULL;}newNode->data = x;newNode->next = NULL;return newNode;
}

注意,单链表是不需要单独初始化的,因为我们每增加一个数据结点,都会动态申请一个空间,在申请的同时会初始化该结点,所以封装成一个函数,方便调用。

2.尾插和头插

现在介绍的是不带哨兵位头结点的单链表,后面的pphead都是单链表头结点地址,这里的头结点不是指哨兵位头结点,而是指链表的第一个结点,表头的位置,二者是不一样的,具体会在后面介绍双链表时细说。

单链表尾插是将新申请的结点链接到尾结点的后面,但不能直接访问链表的尾结点,因此需要从头结点开始遍历链表到尾结点,然后链接新节点。
在这里插入图片描述尾插分为两种情况:1.链表为空;2.链表非空

//尾插
void SLNodePushBack(SLNode** pphead, DataType x)
{assert(pphead);//防止传错成空指针NULL //*pphead无需assert断言,链表可以为空SLNode* newNode = BuySLNode(x);if (NULL == *pphead)//空链表情况{*pphead = newNode;}else{//从第一个结点开始遍历到尾节点SLNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}//在尾结点后面插入新结点tail->next = newNode;}
}

注意:链表在传入头结点(第一个结点)时,由于头结点是一个结构体指针类型,函数体中会修改头结点,所以要用二级指针来接收,否则形参的改变不会影响实参。

链表头插是在新结点的后面链接头结点,然后更新头结点。
在这里插入图片描述

//头插
void SLNodePushFront(SLNode** pphead, DataType x)
{assert(pphead);//防止传错成空指针NULL //空链表可以头插,*pphead无需assert断言SLNode* newNode = BuySLNode(x);newNode->next = *pphead;//头结点为空也适用*pphead = newNode;//头插后更新头结点为第一个的结点
}

总结:

插入不需要assert断言链表是否为空,因为空链表也可以进行插入操作,只需断言传入的参数是否为空,因为要对pphead解引用,NULL不能解引用。

3.尾删和头删

尾删需要两个指针,prev用来指向尾结点的前一个结点,tail用来指向尾结点。
在这里插入图片描述
尾删同样需要分两种情况处理:1.链表只有一个结点;2.链表有两个结点及以上

//尾删
void SLNodePopBack(SLNode** pphead)
{assert(pphead);//防止传错成空指针NULL assert(*pphead);//空链表不可删除if (NULL == (*pphead)->next)//头结点下一个结点为空,则只有一个结点{free(*pphead);//删除结点释放空间*pphead = NULL;}else	//两个以上结点{SLNode* prev = NULL;SLNode* tail = *pphead;while (tail->next != NULL)//找到尾结点的前一个结点{prev = tail;tail = tail->next;}prev->next = NULL;free(tail);tail = NULL;}
}

头删则不用考虑链表是否只有一个结点,只需要将头结点指向下一个结点即可。但是在改变头结点的指向之前要记得保存原来的头结点,是为了释放要删除的结点空间。
在这里插入图片描述

//头删
void SLNodePopFront(SLNode** pphead)
{assert(pphead);//防止传错成空指针NULL assert(*pphead);//空链表不可删除SLNode* head = *pphead;//保存头结点 后续释放*pphead = head->next;//头结点指向第二个结点free(head);//释放刚刚保存的头结点head = NULL;//置空,防止野指针
}

总结:

1.删除操作需要保证链表不为空,所以pphead和*pphead都需要断言判断。
2.结点删除后,需要及时free释放,防止内存泄漏;并置空,防止野指针的形成。

4.打印

遍历一遍链表,将每个结点的data打印。这里以int类型示例。

//打印链表
void SLNodePrint(SLNode* phead)
{//空链表可以打印,无需assert断言SLNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;//切忌cur++}printf("NULL\n");
}

5.查找

遍历链表,查找值为x的结点并返回该结点的指针

//查找
SLNode* SLNodeFind(SLNode* phead, DataType x)
{SLNode* cur = phead;while (cur != NULL){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

6.插入

插入分为两种情况插入:

1.在pos位置之前插入
在这里插入图片描述

在pos位置之前插入,prev用来找到pos的前一个结点,在prev的后面尾插新结点即可。

//插入pos位置之前
void SLNodeInsert(SLNode** pphead, SLNode* pos, DataType x)
{assert(pphead);//防止传错成空指针NULL assert(pos);//检查要插入的位置是否存在assert(*pphead);if (pos == *pphead)//在头结点之前插入相当于头插{SLNodePushFront(pphead, x);//头插}else{SLNode* prev = *pphead;while (prev->next != pos)//找到pos位置的前一个结点{prev = prev->next;}SLNode* newNode = BuySLNode(x);prev->next = newNode;newNode->next = pos;}
}

2.在pos位置之后插入
在这里插入图片描述
这种情况不需要找到pos的前一个结点,直接在pos后面链接新结点。

//插入pos位置之后
void SLNodeInsertAfter(SLNode* pos, DataType x)
{assert(pos);SLNode* newNode = BuySLNode(x);//这两行顺序不能反,否则pos的下个结点会找不到newNode->next = pos->next;pos->next = newNode;
}

注意:因为没有用到头结点,所以头结点可以不用传参。因为只改变了pos结点的next的指向,pos结点本身的值并没有改变,所以不需要用二级指针接收。

总结:

1.在pos位置之前插入,则不能进行尾插;在pos位置之后插入,则不能进行头插。
2.插入时要对pos进行检查,pos不能为空。

7.删除

删除pos指针指向的结点

//删除pos位置的结点
void SLNodeErase(SLNode** pphead, SLNode* pos)
{assert(pphead);//防止传错成空指针NULL assert(pos);//检查要插入的位置是否存在assert(*pphead);if (*pphead == pos)//头删{SLNodePopFront(pphead);}else{SLNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);//pos = NULL; pos是形参,改变对实参无影响;可以传二级指针,或者在外部置空}
}

注意:pos指向的结点free掉之后,为了防止野指针的形成,需要置空处理。 如果传参时pos用的一级指针接收,则需要在外部调用SLNodeErase函数后,自行置空;如果要在函数内部free的同时置空,则需要用二级指针接收pos的地址。按喜好自行选择任意一种即可。

8.销毁

//销毁链表
void SLNodeDestroy(SLNode** pphead)
{assert(pphead);SLNode* cur = *pphead;while (cur != NULL){SLNode* next = cur->next;//先保存下一个结点再销毁free(cur);cur = next;}*pphead = NULL;
}

链表使用完毕不要忘了释放空间。

至此,单链表的内容总结完毕。接下来介绍带头双向循环链表的实现。

四、带头双向循环链表

在这里插入图片描述1.“带头”是指带有哨兵位头结点,与前面单链表中的头结点(链表第一个结点)不一样,哨兵位头结点不存储数据,并且next指向链表的第一个结点。这种情况就不需要单独考虑头结点是否为空。代码也更容易写。
2.双向链表则比单向链表多一个prev指针,指向前一个结点。
3.循环链表中,尾结点的后驱结点指向头结点;双向循环链表中,头结点的前驱结点指向尾结点,尾结点的后驱结点指向头结点,整体构成一个环。

双链表的定义

typedef int DataType;
typedef struct DListNode
{DataType data;struct DListNode* prev;//存储前一个结点的地址struct DListNode* next;//存储后一个结点的地址
}DLNode;

1.双链表的创建

//申请结点
DLNode* BuyDLNode(DataType x)
{DLNode* newNode = (DLNode*)malloc(sizeof(DLNode));if (NULL == newNode){perror("malloc");return NULL;}newNode->data = x;newNode->prev = NULL;newNode->next = NULL;return newNode;
}

2.初始化

初始化将头结点的前驱结点和后驱结点都指向自身

//双链表初始化
DLNode* DLInit()
{DLNode* phead = BuyDLNode(-1);phead->prev = phead;phead->next = phead;return phead;
}

3.判断链表是否为空

链表为空的条件是:头结点的前驱结点和后驱结点都指向自身,判断一个就行。

bool DLEmpty(DLNode* phead)
{assert(phead);return phead->next == phead;//或者return phead->prev == phead;
}

4.尾插和头插

尾插的时要注意,先将新结点和尾结点链接起来,再将新结点和头结点链接起来,否则先链接头结点会找不到尾结点。但是如果先将尾结点保存起来,则不用按照顺序。
在这里插入图片描述

//尾插
void DLPushBack(DLNode* phead, DataType x)
{assert(phead);//phead不能为空,否则说明传错了DLNode* newNode = BuyDLNode(x);DLNode* tail = phead->prev;//一步找到尾结点tail->next = newNode;newNode->prev = tail;newNode->next = phead;phead->prev = newNode;
}

头插也是一样,如果先将第一个结点保存起来,则不需要注意链接顺序。
在这里插入图片描述

//头插
void DLPushFront(DLNode* phead, DataType x)
{assert(phead);DLNode* newNode = BuyDLNode(x);DLNode* newNode first = phead->next;//保存第一个结点first->prev = newNode;newNode->next = first;newNode->prev = phead;phead->next = newNode;
}

注意:带哨兵位头结点的链表,传参时不需要用二级指针接收,因为从始至终都不会改变头结点本身的值。

5.尾删和头删

在这里插入图片描述

//尾删
void DLPopBack(DLNode* phead)
{assert(phead);//链表非空才能删除assert(!DLEmpty(phead)); //或者写成assert(phead->next != phead);DLNode* tail = phead->prev;//保存尾结点DLNode* prev = tail->prev;//保存尾结点的前一个结点phead->prev = prev;prev->next = phead;free(tail);tail = NULL;
}

在这里插入图片描述

//头删
void DLPopFront(DLNode* phead)
{assert(phead);assert(!DLEmpty(phead));//链表为空不能删DLNode* first = phead->next;//保存第一个结点DLNode* next = first->next;//保存第一个结点的下一个结点phead->next = next;next->prev = phead;free(first);first = NULL;
}

6.查找

查找值为x的结点并返回指向该结点的指针

//查找
DLNode* DLFind(DLNode* phead, DataType x)
{assert(phead);DLNode* cur = phead->next;//cur指向第一个结点while (cur != phead){if (cur->data == x)return cur;cur = cur->next;}return NULL;//找不到则返回空
}

注意:查找结束的条件是当前结点不等于头结点,相等说明进入下一轮循环。

7.插入

在pos之前插入新结点

//pos位置之前插入
void DLInsert(DLNode* pos, DataType x)
{assert(pos);DLNode* newNode = BuyDLNode(x);DLNode* prev = pos->prev;prev->next = newNode;newNode->prev = prev;newNode->next = pos;pos->prev = newNode;
}

实现该函数后,头插和尾插可以用一行代码解决:

DLInsert(phead->next, x);//头插:在头结点的下个结点(第一个结点)之前插入
DLInsert(phead, x);//尾插:在头结点的前面(尾结点的后面)插入

8.删除

//pos位置删除
void DLErase(DLNode* phead, DLNode* pos)
{assert(pos);assert(pos != phead);//不能删除头结点pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);//外部置空
}

同样,实现该函数后,头删和尾删可以用一行代码解决:

DLErase(phead, phead->next);//头删
DLErase(phead, phead->prev);//尾删

9.销毁

//链表销毁
void DLDestroy(DLNode* phead)
{assert(phead);DLNode* cur = phead->next;while (cur != phead){DLNode* next = cur->next;//先保存待销毁结点的下一个结点free(cur);cur = NULL;cur = next;}
}

五、总结

1.通过本篇,掌握不带头单向非循环链表和带头双向循环链表之后,大概也能类推写出其他六种链表结构。
2.不带头的链表传参数需要二级指针来接收,因为函数中可能会改变头结点(第一个结点)的值,例如链表为空时的操作。带头的链表传参时只需一级指针接收即可,因为并不会改变头结点(哨兵位头结点)的值。
3.双向链表插入删除操作时,如果不事先保存结点的前后结点,则需要注意链接顺序,否则会找不到原来的结点。
4.在pos位置进行删除操作时,要么传入二级指针置空,要么在外部调用函数完自行置空,否则会形成野指针。
5.链表的头插尾插和头删尾删可以不用写,都可以用插入和删除函数实现。

完整代码放在gitee上:
单链表
双链表

链表和顺序表的对比

顺序表存储方式是顺序存储,即在内存中连续存储数据元素,通过下标来访问元素。

链表存储方式是链式存储,即数据元素在内存中不一定连续,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表的优点:
1.任意位置插入和删除只需修改指针指向,效率很高,时间复杂度为O(1)。
2.需要一个结点就申请一个空间,空间利用率高。
链表的缺点:
不支持随机访问,对结点的操作需要从头指针开始遍历,效率低。

顺序表的优点:
可以根据下标随机访问任意位置的元素,效率为O(1),尾插尾删效率很高。
顺序表的缺点:
1.任意位置插入和删除需要移动元素位置,效率很低,时间复杂度为O(N)。
2.扩容有一定的消耗,可能会有一定空间的浪费。

可以发现,顺序表和链表的优缺点是互补的。对于数据元素较少,访问频繁,插入、删除情况较少的场景选择顺序表,对于数据元素较多,插入、删除操作频繁的场景选择链表。

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

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

相关文章

[力扣题解]93. 复原 IP 地址

题目:93. 复原 IP 地址 思路 回溯法; 特别的是,用pointNum来记录.的数量,并且没有创建path,而是直接在原来的strings中插入.; 同时,在判断子串合法性的时候,0是合法的,…

Java中使用alibaba的easyexcel中的方法实现csv模板下载功能

系列文章目录 文章目录 系列文章目录一、EasyExcelUtil工具 一、EasyExcelUtil工具 /*** param response 响应* param fileName 文件名称* param sheetName sheet名称* param headNameList 头部名称* param <T>* throws IOException*/public static <T>…

基于Springboot+Vue的Java项目-车辆管理系统开发实战(附演示视频+源码+LW)

大家好&#xff01;我是程序员一帆&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &am…

Spring自动装配:解析原理与实践

在Spring框架中&#xff0c;自动装配是一种强大的特性&#xff0c;它能够根据一定的规则自动地将bean装配到Spring容器中&#xff0c;从而简化了配置和开发过程。本文将深入探讨Spring自动装配的原理和实践&#xff0c;帮助程序员更好地理解和应用这一重要特性。 1. 什么是自动…

35个矩阵账号,如何通过小魔推打造2704万+视频曝光?

在如今的短视频时代&#xff0c;矩阵发布的作用被发挥到极致&#xff0c;通过各个短视频平台的流量分发&#xff0c;虽然视频质量不如那些头部的IP&#xff0c;但是在视频数量上却能做到轻松碾压&#xff0c;让自己的品牌与门店有更多的声量&#xff0c;这就是如今短视频平台对…

安卓实现视频录制与显示和翻转摄像头

权限&#xff1a; <!-- 相机权限 --> <uses-featureandroid:name"android.hardware.camera"android:required"false" /> <uses-permission android:name"android.permission.CAMERA" /><!-- 录音权限&#xff08;包括麦克…

2024好用的网页客服系统推荐?

2024好用的网页客服系统推荐&#xff1f;Zoho SalesIQ是一款强大的实时聊天工具&#xff0c;专为网站和在线商店设计。它提供了一套全面的功能&#xff0c;帮助企业实时解决客户问题&#xff0c;提高转化率和客户满意度。 实时监控 Zoho SalesIQ能够实时监控网站的访问者活动&…

能源系统升级BACnet IP分布式I/O边缘模块深度整合

能源管理系统(EMS)的高效运行成为了实现绿色建筑、节能减排的关键。而BACnet IP分布式远程I/O模块作为这一系统中的重要组件&#xff0c;正发挥着不可小觑的作用。本文将以某大型商业综合体为例&#xff0c;探讨BACnet IP I/O模块如何在能源管理中大显身手。 商业综合体涵盖办公…

波分系统中的EDFA光纤放大器

功能&#xff1a; 实现C波段光信号整体放大总波长范围覆盖1528~1565nm支持系统实现不同跨段的无电中继传亮点&#xff1a; 宽增益范围&#xff1a;1528nm~1565nm三种光放大器C波段应用&#xff1a; BA功率放大器LA线路放大器PA前置放大器 低噪声系数&#xff0c;典型值&#xf…

一文教你在windows上实现ollama+open webui、外网访问本地模型、ollama使用GPU加速

前言&#xff1a; ollama工具的出现让大语言模型的部署变得格外的轻松&#xff0c;但是在windows系统部署之后发现无法使用GPU进行加速&#xff0c;通过多方面查找资料发现可以在docker中使用命令启用GPU加速。另外通过Docker也可以快速部署open webui,于是本文基于docker实现…

cmake进阶:文件操作之写文件

一. 简介 cmake 提供了 file() 命令可对文件进行一系列操作&#xff0c;譬如读写文件、删除文件、文件重命名、拷贝文件、创建目录等等。 接下来 学习这个功能强大的 file() 命令。 本文学习 CMakeLists.txt语法中写文件操作。 二. cmake进阶&#xff1a;文件操作之写文件…

【活动】如何通过AI技术提升内容生产的效率与质量

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 如何通过AI技术提升内容生产的效率与质量引言一、自然语言处理&#xff08;NLP&…

预约咨询小程序源码搭建/部署/上线/运营/售后/更新

包含在线咨询、视频咨询、电话咨询、面询多种咨询方式&#xff0c;适用于心理、法律、宠物等预约咨询问诊场景 分类预览&#xff1a;小程序提供清晰的分类选项&#xff0c;使用户能够迅速找到所需的咨询服务类型&#xff0c;如法律咨询、心理咨询、医疗咨询等。预约时间选择&a…

HTML_CSS学习:定位

一、相对定位 相关代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>相对定位</title><style>.outer{width: 500px;background-color: #999ff0;border: 1px solid #000;p…

网线 网口 绿灯:链路连接,黄灯:数据信号 ——网络

正常情况&#xff1a;绿灯常亮&#xff0c;橙色常亮或闪烁。有信号传输时闪烁&#xff0c;没信号时常亮。 绿灯是链路指示&#xff08;即连接&#xff09;&#xff0c;黄灯是信号指示&#xff08;即传数据&#xff09; 一.分情况说明: 1.黄灯闪动,绿灯长亮:网线正常,正在通信中…

十四、网络编程

目录 一、网络编程概述二、网络通讯要素1、IP和端口号2、网络通信协议 三、IP和端口号四、网络协议1、网络通信协议2、TCP/IP协议簇1&#xff09;TCP协议2&#xff09;UDP 3、Socket 五、TCP网络编程1、基于Socket的TCP编程1&#xff09;客户端创建socket对象2&#xff09; 服务…

SQL运算符以及使用变量参与实际的查询(增删改)操作

#关系运算符&#xff08;<>!not&#xff09;与逻辑运算符&#xff08;&and|or&#xff09;set x7,y5;select x>y as TRUE,x<y as FALSE;#正确返回值true,实际返回值1&#xff0c;false0select xy;# 判断两个数值是否相等无需使用&#xff08;&#xff09;号&am…

HTML中插入图片(2024/5/10)

背景&#xff1a; 自己做了个小网站&#xff0c;想在网页的右下角贴上自己的微信二维码&#xff0c;用以下代码就可以了。 注意&#xff0c;这里是放右下角了&#xff0c;距离和二维码的图片大小需要自己去调整。 /*二维码的名字和路径需要自己修改*/ <!DOCTYPE html>…

C++初阶学习第六弹——string(1)——标准库中的string类

前言&#xff1a; 在前面&#xff0c;我们学习了C的类与对象&#xff0c;认识到了C与C语言的一些不同&#xff0c;今天&#xff0c;我们将进入C的 关键部分——STL&#xff0c;学习完这部分之后&#xff0c;我们就可以清楚的认识到C相比于C语言的快捷与便利 一、为什么有string…

获取文件夹下的vue文件形成组件,require.context

前言&#xff1a;项目中现有一个文件里面包含所有需要用到的组件&#xff0c;如果一个个的去import&#xff0c;则会非常麻烦&#xff0c;现有require.context去实现&#xff0c; 1、require.context var request require.context(‘./module’, true, /.js$/) require.cont…