【数据结构】单链表:数据结构中的舞者,穿梭于理论与实践的舞池

 欢迎来到白刘的领域   Miracle_86.-CSDN博客

         系列专栏   数据结构与算法

先赞后看,已成习惯

   创作不易,多多支持!

一、链表的概念和结构

1.1 链表的概念

在上一篇文章中,我们了解了线性表(linear list),并且学习了其中一种线性表——顺序表(Sequence List)链表也是线性表的一种,那么它是一种什么样的结构呢?

链表,顾名思义,带着链子的表。日常生活中,我们知道链子是用来链接两个东西的,那么我们可以很容易理解,顺序表是基于数组实现的,是一个元素一个元素挨着的,那链表我们就可以理解为每个元素用链子连接起来,这样就形成了链表(Linked List)。

1.2 链表的结构

那么我们得到了两个链表的组成的关键元素,一个是每个元素,我们管它叫节点(或结点),另一个就是那个链子。而在C语言中,我们用结构体来实现节点,用指针来充当链,连接两个节点。

在生活中链表类似于我们的火车的结构,在物理上它是一种存储结构非顺序非连续的结构。

节点的组成主要由两个部分组成:一个是数值域,用来保存当前节点的数据,一个是指针域,用来保存下一个节点的地址(指针变量)。

如图所示,指针变量plist保存的是第一个节点的地址,如果我们想让plist指向第二个节点,我们只需要将plist保存的内容修改为0x0012FFD0。

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

因为之前我们说了,链表不同于顺序表,它在内存中的地址不一定是连续的,它是独立申请的(对其插入数据时才申请一个节点)。我们需要通过指针才能从当前节点找到下一个节点。

所以我们可以通过一个结构体来实现一个节点,它要有一个节点的数据以及一个指针变量来保存下一个节点的地址,代码如下:

struct SListNode
{int data; //节点数据struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};

当然我们也可以用typedef来进行修改,方便我们后续操作。

typedef int SLTDataType;typedef struct SListNode
{SLTDataType data; //节点数据struct SListNode* next; //指针保存下⼀个节点的地址
}SLTNode;

我们如何将链表进行打印呢?

首先我们将节点传到打印函数,然后我们创建了一个指向头结点的结构体指针,利用循环从头遍历到尾,每次打印完成令pcur指向pcur的next节点。这里注意的就是结构体成员的访问的问题,我们用到了->操作符,在前面的博客我们都介绍过。

武器大师——操作符详解(下)-CSDN博客

代码如下:

void SLTPrint(SLTNode* phead){SLTNode *cur = phead;while(cur){printf("%d ",cur->data);cur = cur->next;}printf("\n");
}

 二、单链表的实现

上面我们知道了什么是链表,那单链表又是什么呢?单链表(Singly Linked List),一般所指的是“不带头单向不循环链表”。

我们实现的功能无非就四个“增、删、查、改”。

2.1 增

2.1.1 尾插

尾插,顾名思义,在尾部插入,俗称后入(bushi)。

首先我们要申请新的节点,我们可以写一个申请节点的函数。

SLTNode* BuyNode(SLTDataType x) {SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));if (node == NULL) {perror("malloc fail!\n");exit(1);}node->data = x;node->next = NULL;return node;
}

首先我们用malloc函数动态申请一块内存,然后将节点的数据赋进去。

void SLTPushBack(SLTNode** pphead, SLTDataType x) {assert(pphead);SLTNode* node = BuyNode(x);if (*pphead == NULL) {*pphead = node;return;}//找尾SLTNode* cur = *pphead;while (cur->next) {cur = cur->next;}cur->next = node;
}

接下来的代码逻辑特别简单,首先我们通过buynode函数创建了一个节点,然后我们判空,如果这个链表是空的,我们直接将指针变量pphead赋为node。如果不是空的,那我们就需要找到链表的尾部,我们可以通过循环来找到尾部,首先为了防止原来数据被破坏,我们创建一个指向第一个节点的指针变量cur,而不是直接遍历pphead。

注意我们如何找尾,通过判断该节点的下一个位置是否为空。

这里还有一个细节就是,我们传入的是二级指针,因为我们要修改一个数据的话,需要传地址,而那个数据如果是指针的话,我们就需要传指针的地址,也就是二级指针。

2.1.2 头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {assert(pphead);SLTNode* node = BuyNode(x);node->next = *pphead;*pphead = node;
}

这个代码逻辑也很简单,首先我们assert断言防止访问空指针,之后buynode函数申请节点,然后将结点接到头部,也就是*pphead,之后别忘了将*pphead再指向node,形成新的头。

2.2 删

2.2.1 尾删

尾删也很简单,仔细思考,我们只需要两步就能完成,一个是找尾,一个是删除。

首先我们来看找尾,我们可以用循环,再加上两个指针,

   //找尾
//    SLTNode *cur = *pphead;
//    SLTNode *prev = NULL;
//    while(cur->next){
//        prev = cur;
//        cur = cur->next;
//    }
//    prev->next = NULL;
//    free(cur);
//    cur = NULL;

首先cur指向头,prev指向空,cur用来找尾,prev用来保存上一个节点的地址。当cur的下一个位置为NULL时循环结束,意味着找到最后一个节点了,我们将它所指的位置free掉(因为是动态开辟出来的),然后指针置空即可完成删除

还有一种方法可以用一个指针就解决,

  SLTNode* tail = *pphead;while (tail->next->next) {tail = tail->next;}free(tail->next);tail->next = NULL;

 我们判断条件改成这样,实际上我们找的tail是倒数第二个节点,所以最后的删除需要改成tail的next为NULL。

2.2.2 头删

头删的逻辑就更加简单了,我们只需要创建一个变量来保存原来的头节点,方便我们后续删除,然后让原头节点移到下一个结点成为新的头结点,然后删掉原来的。如果我们不保存,我们没有办法找到原来的头结点。

代码如下:

void SLTPopFront(SLTNode** pphead) {assert(pphead);assert(*pphead);SLTNode* first = *pphead;*pphead = (*pphead)->next;free(first);first = NULL;
}

2.3 查

这个实现也很简单,就是遍历,然后匹配。由于是查找,而不是对数据进行修改,所以传一级指针即可。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {assert(phead);SLTNode* cur = phead;while (cur) {if (cur->data == x) {return cur;}cur = cur->next;}return NULL;
}

2.4 改

2.4.1 在pos前插入

将图画出来我们就清晰明了了。我们如果想把node插入到pos前,我们只需要将node的next指向pos,然后将prev的next指向node,这样就构成了插入操作。

试想一下,1和2的操作可以调换顺序吗?答案是不可以,这是因为如果我们先做2,这个时候我们就找不到pos了,也就不能执行1操作了。有人会问,我创建两个变量来记录这两个点的位置不可以吗?可以,但是没必要。

之后我们来看代码部分,首先我们要创建一个node节点,用buynode函数。正常情况,我们需要找pos的前一个节点prev,利用循环即可。如果只有一个节点怎么办呢?这个时候直接头插就可以了。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {assert(pphead);assert(pos);SLTNode* node = BuyNode(x);if (*pphead == pos) {//        node->next = pos;//        *pphead = node;SLTPushFront(pphead, x);return;}//找pos的前一个节点SLTNode* prev = *pphead;while (prev->next) {if (prev->next == pos) {break;}prev = prev->next;}node->next = pos;prev->next = node;
}
2.4.2 删除pos

其实逻辑也是很简单的,要删除pos的话,我们需要找到pos前面的节点保存,否则就找不到了。

void SLTErase(SLTNode** pphead, SLTNode* pos) {assert(pphead);assert(pos);if (*pphead == pos) {//        SLTNode *del = *pphead;//        *pphead = (*pphead)->next;//        free(del);//        del = NULL;SLTPopFront(pphead);return;}//找pos的前一个节点SLTNode* prev = *pphead;while (prev->next) {if (prev->next == pos) {break;}prev = prev->next;}prev->next = pos->next;free(pos);//    pos = NULL;//没有存在的必要
}

并且要注意连接好pos后面的节点后再删除,不然也找不到。

2.4.3 在pos后插入

逻辑跟在pos前插入一样,也要注意顺序,只不过在pos之后插入不需要传链表第一个节点了,只需要传pos即可。

void SLTInsertAfter(SLTNode* pos, SLTDataType x) {assert(pos);SLTNode* node = BuyNode(x);node->next = pos->next;pos->next = node;
}
2.4.4 删除pos后的节点
void SLTEraseAfter(SLTNode* pos){assert(pos);assert(pos->next);SLTNode *del = pos->next;pos->next = del->next;free(del);del = NULL;
}

其实看到这你就发现,对链表的操作无非就是保存节点,然后连接,同时画图操作也可以对我们学习数据结构有所帮助。

2.5 链表的销毁

我们将链表每个节点通过循环遍历free掉即可,唯一比较吃操作的就是创建一个用于保存下一个节点的位置的指针。

//销毁链表
void SListDesTroy(SLTNode** pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}

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

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

相关文章

Spring——IOC创建对象方式

可参考官网:https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.htmlhttps://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html 1. 使用无参构造创建对象&#xff0…

数据库性能优化系统设计

设计一个数据库性能优化系统,目标是监测、诊断并改善数据库的运行效率,确保系统能够高效稳定地处理大量数据请求。以下是一个概要设计,包括关键模块、功能和实现思路: 1. 系统架构 分布式监控中心:采用分布式架构收集…

PHP宝藏神器多功能投票系统源码小程序

🎉发现宝藏神器!一键解锁“多功能投票小程序”的无限可能✨ 🌈 开篇安利:告别繁琐,拥抱高效! Hey小伙伴们,是不是经常为组织活动、收集意见而头疼不已?🤯 今天就要给大…

Node 中基于 Koa 框架的 Web 服务搭建实战

前言 在《Node之Web服务 - 掘金 (juejin.cn)》一文中,我们使用 HTTP 模块构建了后端接口,从而实现了后端服务的开发。可以对此进行进一步优化 http模块代码回顾 const http require("http");const server http.createServer((req, res) > {if (reqUrl.pathna…

Python前沿技术:机器学习与人工智能

Python前沿技术:机器学习与人工智能 一、引言 随着科技的飞速发展,机器学习和人工智能(AI)已经成为了计算机科学领域的热门话题。Python作为一门易学易用且功能强大的编程语言,已经成为了这两个领域的首选语言之一。本…

【零基础】学JS

喝下这碗鸡汤 “知识就是力量。” - 弗朗西斯培根 1.三元运算符 目标:能利用三元运算符执行满足条件的语句 使用场景:其实是比if双分支更简单的写法,可以使用三元表达式 语法:条件 ? 满足条件的执行代码 : 不满足条件执行的代码 接下来用一个小案例来展…

C#实现求解函数导数和值

using MathNet.Symbolics; using System; using System.IO; using System.Text;private string ConvertToLatex(string mathExpression) {return mathExpression.Replace(" * ", "").Replace("*", ""); }// 将函数定义为字符串 string…

AI语言处理的双刃剑:Tokens令牌化技术解析

生成式人工智能模型,如GPT-4o,采用基于Transformer架构的复杂处理方式,这与人类处理文本的方式存在明显差异。这些模型依赖于一种称为“令牌化”的过程,将文本分解为更小的片段,称为“令牌”,以便更有效地处…

Kafka抛弃Zookeeper后如何启动?

Kafaka如何下载 官网地址 目前Kafka最新的版本就是3.7.1 我们可以看到下面这两个版本信息?什么意思呢? Scala 2.12 - kafka_2.12-3.7.1.tgz (asc, sha512)Scala 2.13 - kafka_2.13-3.7.1.tgz (asc, sha512) 我们应该知道,一个完整的Kafka实…

平安消保在行动 | 守护每一个舒心笑容 不负每一场双向奔赴

“要时刻记得以消费者为中心,把他们当做自己的朋友,站在他们的角度去思考才能更好地解决问题。” 谈及如何成为一名合格的消费者权益维护工作人员,平安养老险深圳分公司负责咨诉工作的庞宏霄认为,除了要具备扎实的专业技能和沟通…

MySQL篇四:表的约束

文章目录 前言1. 空属性2. 默认值3. 列描述4. zerofill5. 主键6. 自增长7. 唯一键8. 外键 前言 真正约束字段的是数据类型,但是数据类型约束很单一,需要有一些额外的约束,更好的保证数据的合法性,从业务逻辑角度保证数据的正确性。…

软件架构之开发方法

软件架构之开发方法 第6章:开发方法6.1 软件生命周期6.2 软件开发模型6.2.1 瀑布模型6.2.2 演化模型6.2.3 螺旋模型6.2.4 增量模型6.2.5 构件组装模型 6.3 统一过程6.4 敏捷方法6.4.1 极限编程6.4.2 特征驱动开发6.4.3 Scrum6.4.4 水晶方法6.4.5 其他敏捷方法 6.5 软…

【嵌入式Linux】<知识点> 虚拟地址空间

前言 在Linux中,新创建的进程都拥有独立的虚拟地址空间。为深入多进程多线程的理解,了解虚拟地址空间分区十分有必要。 一、概念 虚拟地址空间分为4G空间,其中1G为内核区,3G为用户区。虚拟地址空间的地址从0开始,且该…

66.Python-web框架-Django-免费模板django-datta-able的分页的一种方式

目录 1.方案介绍 1.1实现效果 1.2django.core.paginator Paginator 类: Page 类: EmptyPage 和 PageNotAnInteger 异常: 1.3 templatetags 2.方案步骤 2.1创建一个common app 2.2创建plugins/_pagination.html 2.3 其他app的views.py查询方法 2.4在AIRecords.html里…

入门 Vue Router

Vue Router Vue Router插件做了什么&#xff1f; 全局注册 RouterView 和 RouterLink 组件。添加全局 $router 和 $route 属性。启用 useRouter() 和 useRoute() 组合式函数。触发路由器解析初始路由。 标签介绍 RouterView 加载指定页面 <RouterLink to"/home"…

必剪APP视频剪辑的字幕制作方法教程!

你是否还在用pr听一句打一句的制作字幕&#xff1f;你是否还在用ps做字幕然后拉到pr里一句一句的对时间轴&#xff1f;快别用那些老方法啦&#xff0c;繁琐又浪费时间&#xff01;今天给大家推荐一个方便速度的制作字幕的方法&#xff0c;赶快来看看吧&#xff01; 第一步&…

【Altium】AD-网络版一个用户非人为异常占用多个License的解决方法

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 当出现一个用户同时占用多个授权&#xff0c;又无法单独释放一个授权的情况下&#xff0c;该如何解决。 2、 问题场景 一个用户获取网络版授权后&#xff0c;AD会自动重复获取授权&#xff0c;直到该license下所有授…

读书记录《SQL从小白到大牛》01

读书记录《SQL从小白到大牛》01 接地气的书名&#xff0c;内容应当值得一读。 第一篇 SQL基础 01 一些基础概念 SQL是结构化查询语言&#xff08;Structured Query Language&#xff09;&#xff0c;是一套用来输入、更改和查看关系数据库内容的命令。数据库发展经历三个阶…

Python自动化与系统安全

信息安全是运维的根本&#xff0c;直接关系到企业的安危&#xff0c;稍有不慎会造成灾难性的后果。比如经年发生的多个知名网站会员数据库外泄事件&#xff0c;信息安全体系建设已经被提到了前所未有的高度。如何提升企业的安全防范水准是目前普遍面临的问题&#xff0c;主要有…

Greenplum(一)【MPP 架构 数据类型】

1、Greenplum 入门 Greenplum 是基于 MPP 架构的一款分布式分析型数据库&#xff0c;具备关系型数据库的特点&#xff0c;因为它处理的是结构化的数据&#xff0c;同时具备大数据分布式的特点。 1.1、MPP 架构 MPP&#xff08;Massively Parallel Processing&#xff09;架构是…