【数据结构】详解链表结构

目录

  • 引言
  • 一、链表的介绍
  • 二、链表的几种分类
  • 三、不带头单链表的一些常用接口
    • 3.1 动态申请一个节点
    • 3.2 尾插数据
    • 3.3 头插数据
    • 3.4 尾删数据
    • 3.5 头删数据
    • 3.6 查找数据
    • 3.7 pos位置后插入数据
    • 3.8 删除pos位置数据
    • 3.9 释放空间
  • 四、带头双向链表的常见接口
    • 4.1创建头节点(初始化)
    • 4.2pos位置前插入
    • 4.3删除pos位置数据
    • 4.4其他
  • 五、总结

引言

上篇博客已经介绍了顺序表的实现:【数据结构】详解顺序表。最后在里面也谈及了顺序表结构的缺陷,即效率低,空间浪费等等问题,那么为了解决这些问题,于是乎我们引入了链表的概念,下面将对链表结构进行讲解

一、链表的介绍

首先肯定会问,到底什么是链表?链表的概念链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
在结构上其与火车的结构相似,分为一个个节点,再将每个节点连接起来,就形成了一个链表,其大致结构如下:
在这里插入图片描述
但还要几点需要注意

  1. 链式结构在逻辑上是连续的,但在物理空间上不一定是连续的
  2. 这些节点一般是在堆上申请出来的,即使用malloc函数来动态申请空间;
  3. 每当需要增加一个数据时,便可申请一段空间,空间可能连续也可能不连续。

二、链表的几种分类

链表的结构大致可以分为8类,即:带头/不带头单向链表,带头/不带头双向链表,带头/不带头单向循环链表,带头/不带头双向循环链表。 今天我所介绍的是其中最简单的结构和最复杂的结构:

  1. 单向不带头不循环链表:
    在这里插入图片描述
    单向不带头不循环链表结构简单,但实现起来并不简单且复杂度高,所以一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 双向带头循环链表:
    在这里插入图片描述
    带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。表面上看这结构十分的复杂,但在后面实现函数时会发现这种结构会带来很多优势,实现起来反而更简单了,复杂度也大大降低

三、不带头单链表的一些常用接口

定义如下结构体,表示链表的一个节点:

typedef int SLDataType;typedef struct SlistNode
{SLDataType val;//所需保存的数据struct SListNode* next;//结构体指针,指向下一个节点的地址
}SLNode;

3.1 动态申请一个节点

为了使链表在各个函数中都可以使用,所以我们需要动态开辟内存来创建节点,再通过指针将他们相连接。在CreatNode()函数中我们创建节点并将他们初始化:

//动态申请节点
SLNode* CreatNode(SLTDateType x)
{SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));//检测if(newnode == NULL){perror("CreatNode()::malloc");return;}newnode->val = x;newnode->next = NULL;return newnode;
}

3.2 尾插数据

根据一般逻辑,我们想要尾插那就要先创建新节点并找到尾节点。那么我们定义指针tail,然后利用循环找尾节点再链接新节点tail->next = newnode,另外还要额外判断链表为空的情况,此情况直接尾插即可,具体如下:

//尾插
void SLPushBack(SLNode** pplist, SLDateType x)
{SLNode* newnode = CreatNode(x);if (*pplist == NULL){*pplist = newnode;}else{//找尾SLNode* tail = *pplist;while (tail->next != NULL){tail = tail->next;}//链接tail->next = newnode;}
}

3.3 头插数据

头插就比较简单了,只需要注意一点:不额外定义变量时,要先将新节点链到链表,即newnode->next = *pplist然后再改头节点,即*pplist = newnode,如下:

void SLPushFront(SLNode** pplist, SLDateType x)
{assert(pplist);SLNode* newnode = CreatNode(x);newnode->next = *pplist;*pplist = newnode;
}

3.4 尾删数据

同样想要尾删,那就必须先找到尾节点然后释放空间。但释放完空间后,上一个节点的next仍然指向释放空间的地址,这就可能造成越界访问,野指针问题。所以我们还需要记录尾节点的上一个节点tailPrev,然后通过这个指针将此节点next置为NULL。此外还需用assert()检测链表不为NULL分类讨论链表只有一个节点和有多个节点的情况。如下:

//尾删
void SLPopBack(SLNode** pplist)
{assert(pplist && *pplist);SLNode* tailPrev = NULL;SLNode* tail = *pplist;// 1.只有一个节点if (tail->next == NULL){free(tail);*pplist = NULL;}// 2.两个及以上的节点else{//找尾及上一个节点while (tail->next){tailPrev = tail;tail = tail->next;}free(tail);tail = NULL;tailPrev->next = NULL;}
}

3.5 头删数据

头删数据时,链表同样不能为空,另外头删无需判断链表节点数问题,这就比较容易实现了:

void SLPopFront(SLNode** pplist)
{//不为空assert(pplist && *pplist);//记录第一个节点SLNode* first= *pplist;*pplist = (*pplist)->next;free(first);
}

3.6 查找数据

给定一个val,再链表中向后寻找,找到时返回此节点地址pos,未找到返回NULL。我们只需定义一个结构体指针SLNode* cur = plist;,让他向后走,找到val时返回cur,直到cur = NULL时循环结束并返回NULL。因为这里无需改变链表指向,所以可以直接传一级指针。

SLNode* SLFind(SLNode* plist, SLDateType x)
{SLNode* cur = plist;while (cur){//寻找valif (cur->val == x)return cur;cur = cur->next;}return NULL;
}

3.7 pos位置后插入数据

如下图,先创建一个节点newnode,然后将newnode->next指向pos位置的下一个节点,最后将pos->next指向新节点。
在这里插入图片描述
当然pos != NULL

//指定位置插入
void SLInsertAfter(SLNode* pos, SLDataType x)
{assert(pos);SLNode* newnode = CreatNode(x);newnode->next = pos->next;pos->next = newnode;
}

3.8 删除pos位置数据

先通过循环找到pos前一个节点地址posPrev,和后一个节点地址posNext,然后释放pos节点,链接posPrevposNext
在这里插入图片描述
同样pos != NULL,还有一点是当pos为头节点,就相当于头删,但无需判断,同样适用。

void SLErase(SLNode* pos, SLNode** pplist)
{assert(pos && pplist);SLNode* posPrev = *pplist, *posNext = pos->next, *cur = *pplist;//找pos前一个节点while(cur != pos){posPrev = cur;cur = cur->next;}//链接posPrev->next = posNext;free(pos);
}

3.9 释放空间

因为这是一个链式结构,且每个节点是malloc动态开辟的,所以最后要将所以节点释放,否则会造成内存泄漏问题。只需定义一个结构体指针SLNode* cur = *pplist,让他向后依次释放节点,直到cur = NULL

void SLDestroy(SLNode** pplist)
{assert(pplist);SLNode* cur = *pplist;while(cur){//记录下一个节点SLNode* next = cur->next;free(cur);cur = next;}
}

四、带头双向链表的常见接口

通过上面对单向不循环链表的介绍,我们不难发现其实单链表的尾插,尾删和指定位置删除其实效率是不高的,时间复杂度为O(n)。而双向带头循环链表是不存在这个问题的,且因为链表带头节点的原因,在函数传参是无需用到二级指针,在实现函数时也会发现很多时候也不需要单独判断链表没节点的情况,因为头节点本身就是一个节点,这也大大降低了代码的难度
双向带头循环链表的每个节点都包含两个指针:一个指向上一个节点,一个指向下一个节点。那么便可这样设计节点:

typedef int DataType;
//节点设计
typedef struct DListNode
{DataType val;struct DListNode* _prev;//指向上一个节点的指针struct DListNode* _next;//指向下一个节点的指针
}DLNode;

4.1创建头节点(初始化)

头节点和其他节点的结构是相同的,就相当于链表自带一个节点,并将此节点初始化成以下格式:
在这里插入图片描述

DLNode* InitDLNode(DLNode* phead)
{//创建DLNode* head = (DLNode*)malloc(sizeof(DLNode));if (head == NULL){perror("InitDLNode()::malloc");return;}//形成循环结构head->_prev = head;head->_next = head;head->val = -1;return head;
}

4.2pos位置前插入

对于pos位置之前插入,可以先通过pos->_prev找到前一个节点的地址,然后再进行插入操作。因为是双向循环链表的原因,找pos前一个节点也不需要循环,时间复杂度只有O(1)
在这里插入图片描述
事实上当链表无有效节点(即只有头节点)也不需要单独判断,这样降低了代码难度,具体实现代码如下:

//指定位置插入
void DLNodeInsert(DLNode* pos, DataType x)
{assert(pos);DLNode* posPrev = pos->_prev;//pos前一个节点DLNode* newnode = CreatNode(x);//创建新节点//链接posPrev->_next = newnode;newnode->_prev = posPrev;pos->_prev = newnode;newnode->_next = pos;
}

4.3删除pos位置数据

删除pos位置的数据,我们可以通过pos->_prev找到上一个节点的地址,再通过pos->_next找到下一个节点的地址。然后将这两个节点链接起来,并释放pos节点,如下:
在这里插入图片描述
当只有一个头节点时我们还需额外判断一下,代码如下:

void DLNodeErase(DLNode* pos)
{assert(pos);assert(pos->_next != pos);//不能为头节点DLNode* posPrev = pos->_prev, * posNext = pos->_next;//链接posPrev->_next = posNext;posNext->_prev = posPrev;free(pos);
}

4.4其他

还有头删/插,尾删/插这四个函数,但这四个函数并不需要额外实现,因为:
头插/删可以当作pos = phead->_next的指定位置插入/删除,尾删也可以当作pos = phead->_prev的指定位置删除,尾插则是pos = phead的位置。头/尾删这两个pos分别代表头节点的后一个和前一个,且判断assert(phead != phead->_next)也是必要的,这里就不代码实现了。


五、总结

对比于顺序表我们发现链表有很多优点,也有一些缺点:
优点:

  1. 链表的空间浪费较小,按需开辟空间;
  2. 任意位置插入和删除数据效率更高,实现了O(1)时间复杂度;

缺点:

  1. 不支持随机访问数据(致命缺陷);
  2. 每个节点还要存储链接下一个节点的指针,这也会造成一点空间浪费;

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

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

相关文章

有趣的按钮分享

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 广告打完&#xff0c;我们进入正题&#xff0c;先看效果&#xff1a; 废话不多&#xff0c;上源码&#xff1a; <button class&quo…

【LeetCode刷题日志】232.用栈实现队列

&#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;C/C领域新星创作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;LeetCode 刷题日志&#x1f91d;希望作者的文章能对你有所帮助&#xff0c;有不足的地方请在评论区留言指正&#xff0c;…

竞赛 题目:基于机器视觉opencv的手势检测 手势识别 算法 - 深度学习 卷积神经网络 opencv python

文章目录 1 简介2 传统机器视觉的手势检测2.1 轮廓检测法2.2 算法结果2.3 整体代码实现2.3.1 算法流程 3 深度学习方法做手势识别3.1 经典的卷积神经网络3.2 YOLO系列3.3 SSD3.4 实现步骤3.4.1 数据集3.4.2 图像预处理3.4.3 构建卷积神经网络结构3.4.4 实验训练过程及结果 3.5 …

负载均衡简介

负载均衡 负载均衡&#xff08;Load Balance&#xff0c;简称 LB&#xff09;是高并发、高可用系统必不可少的关键组件&#xff0c;目标是 尽力将网络流量平均分发到多个服务器上&#xff0c;以提高系统整体的响应速度和可用性。 负载均衡的分类和OSI模型息息相关&#xff0c…

【广州华锐互动】VR可视化政务服务为公众提供更直观、形象的政策解读

虚拟现实&#xff08;VR&#xff09;技术正在逐渐应用于政务服务领域&#xff0c;为公众提供更加便捷、高效和个性化的服务体验。通过VR眼镜、手机等设备&#xff0c;公众可以在虚拟环境中参观政务服务中心&#xff0c;并根据自己的需求选择不同的办事窗口或事项进行咨询和办理…

03 前后端数据交互【小白入门SpringBoot + Vue3】

项目笔记&#xff0c;教学视频来源于B站青戈 https://www.bilibili.com/video/BV1H14y1S7YV 前两个笔记。是把前端页面大致做出来&#xff0c;接下来&#xff0c;把后端项目搞一下。 后端项目&#xff0c;使用IDEA软件、jdk1.8、springboot2.x 。基本上用的是稳定版。 还有My…

【Linux】vscode远程连接ubuntu失败

VSCode远程连接ubuntu服务器 这部分网上有很多&#xff0c;都烂大街了&#xff0c;自己搜吧。给个参考连接&#xff1a;VSCode远程连接ubuntu服务器 注意&#xff0c;这里我提前设置了免密登录。至于怎么设置远程免密登录&#xff0c;可以看其它帖子&#xff0c;比如这个。 …

51单片机应用

目录 ​编辑 1. C51的数据类型 1.1 C51中的基本数据类型 1.2 特殊功能寄存器类型 2. C51的变量 2.1 存储种类 1. C51的数据类型 C51是一种基于8051架构的单片机&#xff0c;它支持以下基本数据类型&#xff1a; 位&#xff08;Bit&#xff09;&#xff1a;可以表…

WSL 2 更改默认安装的 Linux 发行版

目录 什么是 WSL 2&#xff1f;更改默认安装的 Linux 发行版更改发行版的 WSL 版本 什么是 WSL 2&#xff1f; WSL 2 是适用于 Linux 的 Windows 子系统体系结构的一个新版本&#xff0c;它支持适用于 Linux 的 Windows 子系统在 Windows 上运行 ELF64 Linux 二进制文件。 它的…

单元测试实战(四)MyBatis-Plus 的测试

为鼓励单元测试&#xff0c;特分门别类示例各种组件的测试代码并进行解说&#xff0c;供开发人员参考。 本文中的测试均基于JUnit5。 单元测试实战&#xff08;一&#xff09;Controller 的测试 单元测试实战&#xff08;二&#xff09;Service 的测试 单元测试实战&am…

Flutter 使用 device_info_plus 遇到的问题

问题&#xff1a;引用device_info_plus 插件出现了异常&#xff0c;不知道为啥打开项目的时候就不能用了。 解决&#xff1a;改了版本解决 Target of URI doesnt exist: package:device_info_plus/device_info_plus.dart. (Documentation) Try creating the file reference…

react antd下拉选择框选项内容换行

下拉框选项字太多&#xff0c;默认样式是超出就省略号&#xff0c;需求要换行全展示&#xff0c;选完在选择框里还是要省略的 .less: .aaaDropdown {:global {.ant-select-dropdown-menu-item {white-space: pre-line !important;word-break: break-all !important;}} } html…

uniapp 手动调用form表单submit事件

背景&#xff1a; UI把提交的按钮弄成了图片&#xff0c;之前的button不能用了。 <button form-type"submit">搜索</button> 实现&#xff1a; html&#xff1a; 通过 this.$refs.fd 获取到form的vue对象。手动调用里面的_onSubmit()方法。 methods:…

STM32CubeMX学习笔记-CAN接口使用

STM32CubeMX学习笔记-CAN接口使用 CAN总线传输协议1.CAN 总线传输特点2.位时序和波特率3.帧的种类4.标准格式数据帧和遥控帧从STM32F407参考手册中可以看出主要特性如下CAN模块基本控制函数CAN模块消息发送CAN模块消息接收标识符筛选发送中断的事件源和回调函数 CubeMX项目设置…

OpenAI 地震!首席执行官被解雇,背后的原因是?

11月17日&#xff0c;ChatGPT的制造商OpenAI表示&#xff0c;经过审查后发现联合创始人兼首席执行官 Sam Altman与董事会“沟通时并不一贯坦诚”&#xff0c;因此公司已经决定解雇他。这家人工智能&#xff08;AI&#xff09;公司在一份声明中表示&#xff1a;“董事会不再相信…

基于深度学习的单帧图像超分辨率重建综述

论文标题&#xff1a;基于深度学习的单帧图像超分辨率重建综述作者&#xff1a; 吴 靖&#xff0c;叶晓晶&#xff0c;黄 峰&#xff0c;陈丽琼&#xff0c;王志锋&#xff0c;刘文犀发表日期&#xff1a;2022 年9 月阅读日期 &#xff1a;2023.11.18研究背景&#xff1a; 图像…

一款实用的.NET Core加密解密工具类库

前言 在我们日常开发工作中&#xff0c;为了数据安全问题对数据加密、解密是必不可少的。加密方式有很多种如常见的AES&#xff0c;RSA&#xff0c;MD5&#xff0c;SAH1&#xff0c;SAH256&#xff0c;DES等&#xff0c;这时候假如我们有一个封装的对应加密解密工具类可以直接…

美创科技与南京大数据安全技术有限公司达成战略合作

近日&#xff0c;美创科技与南京大数据安全技术有限公司正式签署战略合作协议&#xff0c;优势力量共享、共拓共创共赢。 美创科技CEO柳遵梁、副总裁罗亮亮、副总裁王利强&#xff0c;南京大数据安全技术有限公司总经理潘杰、市场总监刘莉莎、销售总监王皓月、技术总监薛松等出…

上海亚商投顾:三大指数小幅上涨 HBM概念股全天强势

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数早盘窄幅震荡&#xff0c;午后集体拉升翻红&#xff0c;黄白二线走势分化&#xff0c;题材热点快速轮…

面向未来的自动化:拥抱机器人即服务(RaaS)

01. RaaS是什么&#xff1f; 对于希望实现业务流程自动化的公司来说&#xff0c;机器人通常是一笔巨大的资本支出。由于机器人非常昂贵&#xff0c;公司可能需要等待数年才能看到投资回报。正是由于这一现实&#xff0c;许多较小的组织无法投资机器人。 但一些机器人公司正在采…