C语言数据结构(3)——线性表其二(单链表)

欢迎来到博主的专栏——C语言数据结构
博主id:代码小豪

文章目录

    • 单链表
    • 不连续存储的线性表
      • 单链表
      • 单链表的结构
      • 头指针
      • 单链表的操作
        • 打印单链表
      • 空链表
        • 单链表的插入
          • 尾插法
        • 头插法
      • 单链表的查找
      • 任意位置处的节点插入
      • 单链表节点的删除
    • 销毁链表

单链表

顺序表是一个物理结构上连续(即数据在内存中连续存储)的线性表,这种特性让顺序表的访问非常方便,但是顺序表有一个缺点,就是当我们在顺序表插入或删除数据时,需要较为繁杂的指令执行(时间复杂度O(N))。

为什么会这样呢?因为数据的连续的存储结构虽然为顺序表的数据访问带来了便利,但是当顺序表需要插入元素时,联系的存储结构是没有多余的空间留给待插入的元素的,只能将顺序表中的后继数据依次向后移动,才能腾出空间让数据插入。
在这里插入图片描述
当顺序表删除一个元素时,不能只是单纯的将这个元素移除,因为移除一个元素之后,连续存储的结构就被破坏掉了,为了保持顺序表的结构,只能将后面元素依次向前移动。
在这里插入图片描述
如果我们编写的程序需要频繁的对线性表进行删除、插入的操作时,使用顺序表的数据结构会让程序的运行时间变长。为了解决这个问题,我们需要一个适合插入和删除元素的线性表。

根据上面对顺序表的分析,我们知道导致顺序表插入元素和删除元素的操作如此繁杂的原因就是,顺序表需要时刻保持连续储存的内存结构

那么解决思路来了,如果我们插入元素时,这些元素不需要连续的存储结构,也能有着线性的逻辑结构,那么不就能实现一个插入和删除操作不繁杂的线性表了吗?那么有一个问题就诞生了,如何将不连续存储的数据彼此之间联系起来呢?

不连续存储的线性表

假如你是一个来到广州的游客。你打算去以下几个地方:广州塔、北京路、长隆。如果这几个地方。如果它们顺序的排列在广州站附近就好了,这样子你一出站就能游玩所有的地方,但是现实当中这绝对不可能。

如何达到我们想去的景点呢?我想我会先打开gps,然后定位我打算去的地方,至于交通工具的选择我想就因人而异了。但是不可否认的事,我得先知道景点在哪,我才能去。

那么,C语言中能够定位数据的gps是什么呢?没错,就是指针。如果线性表中的所有元素,使用指针联系起来,那么,这些数据元素就不需要在内存中连续存储了。

那么这种联系的方式是什么呢?我们再以现实举例,如果我想要从广州站导航到广州塔,gps需要得到什么?是不是需要得到我的当前地址(广州站),再得到广州塔的地址,最后给出路线。跟着导航我们就能达到广州塔。

回到线性表,如果一个数据中,存储着下一个数据的地址。那么当我们得到一个数据时,是不是就能找到这个数据的后继元素?

理论形成,开始实践

单链表

根据前面的判断可以得出,这个线性表中的元素不仅仅要存储元素本身的数据,还要存储指向下一个元素的指针,这样子才能通过指针找到下一个元素。

这个线性表的数据关系应该是这样的
在这里插入图片描述
可以发现这种线性表是不需要顺序存储结构的,通过指针完成元素与后继元素的联系,实现线性的逻辑结构。

既然这种线性表的存储结构是无规律的,那么我们就忽视存储结构,直接抽象出这些数据的逻辑结构。

在这里插入图片描述

观察上图,我们发现这种关系图有点像那种打草结,将整个顺序表看做是一个藤蔓,如果想要插入一个数据,就好比在藤蔓上打个结,有多少个元素就有多少个结。所以将这种元素称为节点。而草结与草结之间还有一个藤条联系着,就可以看做数据与数据之间有一个链条联系着。

所以这种数据结构被称为,链表(链条作为联系的顺序表)。链表中的数据被叫做节点。

目前先来介绍的是单链表,单链表是由单向链条而得名,比如图中得到一个数据只能访问后继元素,这种是单向链条,而数据不仅能访问后继元素,也能访问前置元素的被称为双向链表。
在这里插入图片描述

单链表的结构

单链表中的数据要满足两个条件,一是能储存数据本身、二是能存储指向下一个数据的指针。那么C语言中的什么数据类型能够做到这一点呢?首先C语言中的内置类型(int、float、char)肯定是不行的,因为它们只能做到存储元素本身的数据。所以我们要将目光转向结构体类型。

单链表结点的结构体类型如下:

typedef int SLData;
typedef struct SLlistNode
{SLData data;struct SLlistNode* next;
}Node;

data是节点元素存储本身的数据,next是指向下一个结点元素的指针。我们可以手动的写出一个链表。

	Node* node1 = malloc(sizeof(Node));Node* node2 = malloc(sizeof(Node));Node* node3 = malloc(sizeof(Node));Node* node4 = malloc(sizeof(Node));node1->data = 1;node2->data = 2;node3->data = 3;node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;

在这里插入图片描述
线性表中的元素特征如下:除表头和表尾外每个元素都有一个前驱元素和后继元素,表头元素无前驱、表尾元素无后继。

所以node4作为表尾是没有后继元素的,所以node4->next’不指向内存中的任何位置,置为NULL

打开vs中的调试操作,可以发现链表的数据结构如下:
在这里插入图片描述
可以发现、只需要有node1,就可以通过访问next找到其余结点的数据。

头指针

前面的操作一经发现node1的一个重要作用,就是只要能获取node1的位置,就能找到整个链表的数据。

我们将node1称为单链表的第一个结点、指向第一个结点的指针称为头指针、这样子在调用单链表的操作函数时、只需要传入头指针、就能对链表中的所有节点进行访问。
在这里插入图片描述

单链表的操作

打印单链表

为了方便调试和查看单链表、我们可以创建一个打印单链表的函数,前面已经提到了通过头指针可以访问整个链表的特性,所以这个函数只需要一个参数,那就是传入单链表中的头指针

void printSList(Node* phead);

那么该如何遍历整个单链表、访问整个单链表的数据呢?

可以创建一个临时指针pcur,让pcur通过next指针循环访问整个链表。
在这里插入图片描述

循环的结束标志就是访问完链表的表尾,而链表中的表尾元素的next被置为NULL。所以判断结束的条件便是pcur=NULL

函数实现的代码如下:

void printSList(Node* phead)
{Node* pcur = phead;while (pcur){printf("%d->",pcur->data);pcur = pcur->next;}printf("NULL\n");
}

以上面创建的链表为例,调用打印链表的函数

	printSList(node1);

执行结果如下:
在这里插入图片描述

空链表

如果一个链表中没有任何一个节点,就将这个链表称为空链表。

前面提到了头指针是指向链表中第一个节点的指针。但是空链表的头指针应该指向哪儿呢?

头指针是指第一个节点的指针,它的存在是标记一个链表的表头。将头指针传入函数中进行调用,可以对整个链表进行操作。这就说明了一个头指针,是一个单链表的化身。任何一个单链表,都需要有一个头指针作为该单链表的标志。

那么空链表是不是一个单链表,当然是了。这就说明空链表也应该有一个头指针,这个头指针应该指向哪儿呢?空链表是没有第一个节点的,那就只好将头指针置为NULL了。

那么可以得出一个结论,将一个头指针置为NULL,说明该头指针对应的是一个空链表。

单链表的插入
尾插法

在单链表中的表尾该如何插入元素呢?我们知道节点的next指针是指向后继元素的,而尾节点的next指针指向NULL。所以插入元素的方法为:将待插入节点作为当前尾节点的后继元素(即尾节点->next指向待插入元素),并将待插入节点的后继节点位置置为NULL。
在这里插入图片描述

那么我们待解决的问题就是如何遍历珍整个单链表找到尾结点了,尾结点的next指向NULL。我们可以通过这个条件来判断当前节点是否为尾结点。

尾插法的函数原型的参数应该有两个,一是待尾插的单链表是哪个,而是插入的节点数据是什么。找到单链表的方式很简单,只需要传入该链表的头指针就行了。

于是函数原型如下:

void SLlistDataPushBack(Node** pphead, SLData n);

头指针的数据类型是Node*,这里将函数参数作为二级指针的原因如下:

(1)尾插法在特殊情况下是需要对头指针进行修改的,比如当传入的单链表为空链表时,需要将插入的节点设置为第一个节点的,即头指针要从NULL变成指向第一个节点的指针。

(2)既然头指针的数据类型是一个一级指针,那么函数的参数如果也是一个一级指针的话,就不能对头指针进行修改了,因为函数的参数,是实参的一份临时拷贝。也就是说对函数中的头指针进行修改,是不会对传入函数的头指针进行操作的。

(3)那么想要对头指针进行数据修改,就需要将形参升级为二级指针,通过解引用二级指针,才能实现对一级指针的修改。

从这里也能得到一个推论,如果一个单链表的操作函数涉及了头指针的修改的话,就需要将接收这个头指针的形式参数改为一个二级指针。

那么通过上述分析我们已经知道了尾插法在操作时可能会遇到的两种情况。
(1)传入的链表为非空链表时,处理方法如下
在这里插入图片描述

(2)传入的链表为空时,解决方法如下:
在这里插入图片描述

于是尾插法的函数实现如下:

void SLlistDataPushBack(Node** pphead, SLData n)//尾插法
{assert(pphead);Node* pcur = *pphead;Node* pend = malloc(sizeof(Node));//创建待插入元素pend->next = NULL;//尾插法中的待插入元素无后继pend->data = n;if (*pphead==NULL)//判断链表是否为空{(*pphead = pend);//将头指针指向新节点return;}while (pcur->next)//判断是否为尾节点{pcur = pcur->next;}pcur->next = pend;//将待插入的元素作为当前尾结点的后继,完成尾插return;
}

可以用前面写的代码进行测试一下。比如

	Node* plist = NULL;SLlistDataPushBack(&plist, 1);SLlistDataPushBack(&plist, 2);SLlistDataPushBack(&plist, 3);SLlistDataPushBack(&plist, 4);printSList(plist);

运行结果如下:
在这里插入图片描述

头插法

将节点插入至链表的表头称为头插法,节点被插入表头的方法如下
(1)将头指针指向新插入的节点
(2)将原先的第一个节点变成新插入的节点的后继元素
在这里插入图片描述
这个函数中会将头指针进行修改,前面提到了如果函数中出现了修改头指针的情况,那么函数中的形式参数应该设置为头指针的二级指针,这样才能通过解引用操作对头指针进行修改

还要注意一个特殊情况,那就是传入的链表为空链表的情况。如果传入的链表为空链表,那么新插入的节点没有后继,那么这个节点的next应该置为NULL。

于是函数的原型如下:
在这里插入图片描述

void SLlistDataPushFront(Node** pphead, SLData n);

根据上述的原理,函数的实现如下:

void SLlistDataPushFront(Node** pphead, SLData n)
{assert(pphead);//检测头指针的二级指针的合法性Node* pstart = malloc(sizeof(Node));//待插入的节点pstart->data = n;if (*pphead==NULL)//检测链表是否为空表{pstart->next = NULL;*pphead = pstart;return;}pstart->next = (*pphead);//注意这两行代码顺序不能互换*pphead = pstart;
}

注意这两行代码:

pstart->next = (*pphead);
*pphead = pstart;

不能让*pphead先等于pstart,再将pstaet的后继元素设置为(*pphead),原因交给大家思考。

将函数进行测试,方法如下:

	SLlistDataPushFront(&plist, 5);SLlistDataPushFront(&plist, 6);SLlistDataPushFront(&plist, 7);SLlistDataPushFront(&plist, 8);printSList(plist);

在这里插入图片描述

单链表的查找

单链表的查找就很简单了,根据要查找数据项,将整个链表的元素遍历一遍,如果找到就返回一个指向该节点的指针、如果没有找到就返回NULL

函数原型如下:

Node* SLlistFind(Node* phead, SLData n)

具体实现如下:

Node* SLlistFind(Node* phead, SLData n)
{Node* pcur = phead;while (pcur){if ((pcur->data) == n)return pcur;pcur = pcur->next;}return NULL;
}

任意位置处的节点插入

在链表中插入的位置可分为三种
(1)插入表头
(2)插入表尾
(3)插入其他位置

其中前面两种方法都已经说过了,现在来了解一下其他位置的数据插入。

我们要在两个数据之间插入元素,假设要插入的位置为第i+1项,那么原本在第i+1项的节点就要成为新插入节点的后继节点,而第i项节点要成为新插入元素的前驱节点。
在这里插入图片描述
写成代码应该是这样的

newnode->next=nodei->next;
nodei->next=new->node;

要注意这两行的顺序。 如果数据要插入表头,表尾、或者传入空表时,可以用头插法\尾插法的方式实现。

那么现在就剩第二个问题了,就是如何确定待插入元素的位置,我们前面定义了一个查找节点的函数,通过查找数据项确定元素位置。还有一种情况是指定位置插入节点,即不管第3个节点的数据是什么,我这个数据都要插入第三个节点(当然,没有第三个节点位置的另算)。

总之根据实现的要求,寻找位置的方式都是各种各样的,但是插入节点的原理是不变的,这里先来讲解通过指针的方式寻找插入位置的方法(即通过寻找数据项确定位置,因为前面定义了函数嘛)、

void SLlistInsert(Node** Pphead, Node* pos, SLData n);

(1)pphead是链表的头指针的二级指针
(2)pos是待插入节点的地址位置(地址位置可以通过前面编写的查找节点的函数来确定。)
(3)n是待插入节点的数据

void SLlistInsert(Node** pphead, Node* pos, SLData n)
{assert(pphead);if (pos == (*pphead))//第一节点就是要插入的位置{SLlistDataPushFront(pphead, n);return;}Node* pcur = *pphead;Node* newnode = malloc(sizeof(Node));newnode->data = n;while (pcur->next != pos)//找到对应位置才会退出循环{pcur = pcur->next;}newnode->next = pcur->next;pcur->next = newnode;
}

通过测试,发现该函数插入表尾不需要使用尾插法。

实际上根据不同的实现要求,所有写链表的程序都是不唯一的,但是插入节点的原理都是类似。这里需要大家对链表有一个比较深的理解。博主只是列举其一、但是并非唯一的实现方法。

比如有些程序要求对指定位置之前插入节点,有的要求对指定位置之后插入节点,那么此时就要具体问题具体分析了。但是插入的原理不变。只是如何定位的差异。

单链表节点的删除

链表中的节点被删除,节点中的前驱元素和后继元素的关系就被改变了,被删除的节点的前驱节点应该直接指向被删除的后继节点,这样子链表就不会在访问该节点了。

又由于该节点是由动态内存函数开辟的空间,因此该节点被删除时,应该被释放掉。

我们还是使用SLlistFind函数确定删除的节点位置pos。
在这里插入图片描述
从图中可以发现,为了定位到pos的前驱节点,还需要创建多一个pre的临时指针。

函数原型如下:

void SLlistDelete(Node** pphead, Node* pos);

前面将述的方法都是建立在被删除的节点有前驱节点和后继节点的情况下,如果被删除的节点没有前驱节点(第一个节点),就不能用上述方法。

我们针对这个条件在设计一道程序,如过头指针与被删除节点的地址一致,我们就将头指针指向删除节点的后继节点,再释放掉第一个节点的空间。

在这里插入图片描述
函数的实现如下:

void SLlistDelete(Node** pphead, Node* pos)
{assert(pphead);assert(*pphead);if (*pphead == pos)//删除第一个节点{*pphead = (*pphead)->next;free(pos);return;}Node* prev = *pphead;//删除其他位置的节点while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);
}

销毁链表

如果某个链表在程序当中已经不再使用了,由于链表是由动态内存开辟的,要将这个链表的内存空间释放。

释放的方法如下:将链表从第一个节点开始,通过循环将整个链表的节点进行删除。

函数原型:

void SLlistDestory(Node** pphead);

销毁链表的原理也很简单,通过节点之间的联系将整个链表循遍历一遍,每经过一个节点,就将该节点释放。循环结束后将头指针置为NULL

函数的实现如下:

void SLlistDestory(Node** pphead)
{assert(pphead);Node* pcur = *pphead;Node* pnext ;while (pcur){pnext = pcur->next;free(pcur);pcur = pnext;}*pphead = NULL;
}

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

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

相关文章

万字长文详解Java线程池面试题

王有志,一个分享硬核 Java 技术的互金摸鱼侠 加入 Java 人的提桶跑路群:共同富裕的Java人 今天是《面霸的自我修养》第 6 篇文章,我们一起来看看面试中会问到哪些关于线程池的问题吧。数据来源: 大部分来自于各机构(J…

【K8S】Kubernetes 中滚动发布由浅入深实战

目录 一、Kubernetes中滚动发布的需求背景1.1 滚动发布1.2 滚动发布、蓝绿发布、金丝雀发布的区别 二、Kubernetes中实现滚动发布2.1 定义Kubernetes中的版本2.2 创建 Deployment 资源对象2.2.1 在 Yaml 中定义 Deployment 资源对象2.2.2 执行命令创建 Deployment 资源对象 三、…

Asp.net core 框架入门

概述 appsettings.json:配置文件,数据库连接字符串配置信息 Program.cs:程序入口文件(里面有个Main方法) Startup.cs:启动配置文件 依赖项:管理项目所依赖的第三方组件的安装,配…

WampServer

开发笔记 推荐链接php无法保存SESSION问题部署SSL时候产生的问题 推荐链接 链接目录 php无法保存SESSION问题 php.ini文件和phpForApache.ini 文件 里面都有 对路径的控制,相关路径问题可能也需要进行修改,打开文件搜索wamp64或wamp 就可以看到了&…

“深入理解RabbitMQ交换机的原理与应用“

深入理解RabbitMQ交换机的原理与应用 引言1. RabbitMQ交换机简介介绍1.1 什么是RabbitMQ?1.1.1 消息中间件的作用1.1.2 RabbitMQ的特点和优势 1.2 RabbitMQ的基本概念1.2.1 队列1.2.2 交换机1.2.3 路由键 1.3 交换机的作用和分类1.3.1 直连交换机(direct…

VS Code Json格式化插件-JSON formatter

🦪整个文件格式化 按快捷键Shift Alt F 🥪仅格式化选择内容 需要选择完整的json段落即:{} 或 [] 括起来的部分,再按快捷键Ctrl K F

社区公益培训系统功能说明

社区公益培训系统功能说明 本系统将用于社区面向居民开展的公益培训课程展示,在线报名,并按班级排课上课,上课时学员要扫码签到,经常旷课的学员将禁止再报名其他课程。 1. 用户注册与登录 - 提供用户注册和登录功能,…

鸿蒙不再兼容安卓,鸿蒙开发薪资高达4w+,程序员是否需转行鸿蒙?

鸿蒙系统的崛起 鸿蒙系统的推出经历了长时间的研发和完善,它是一款自主研发的操作系统,集成了最新的技术和创新理念。该系统具备卓越的安全性、兼容性和扩展性,因此备受关注。最初,鸿蒙系统主要应用于华为手机产品,但…

惬意上手Python —— 装饰器和内置函数

1. Python装饰器 Python中的装饰器是一种特殊类型的函数,它允许用户在不修改原函数代码的情况下,增加或修改函数的行为。 具体来说,装饰器的工作原理基于Python的函数也是对象这一事实,可以被赋值给变量、作为参数传递给其他函数或者作为其他…

比较有创意的网站

有创意的网站通常展示了独特的设计、交互或内容。以下是一些备受赞誉的有创意的网站,你可以参考: Awwwards: Awwwards 是一个评选并展示全球最优秀网站的平台。你可以在这里找到很多有创意的网站设计。 Awwwards CSS Design Awards: 类似于Awwwards&…

3d gaussian splatting笔记(paper部分翻译)

本文为3DGS paper的部分翻译。 基于点的𝛼混合和 NeRF 风格的体积渲染本质上共享相同的图像形成模型。 具体来说,颜色 𝐶 由沿射线的体积渲染给出: 其中密度 𝜎、透射率 𝑇 和颜色 c 的样本是沿着射线以…

VSCode插件 —— Cody AI (免费AI助手!)

之前介绍过一款 阿里云免费的AI开发工具——通义灵码 TONGYI Lingma 本文再推荐一个可以极大提高开发前端开发效率的工具 —— Cody AI (Sourcegraph),同样是免费的! 不过,使用Cody AI需要有github 或 Google 、 git…

vue3相比vue2的效率提升

1、静态提升 2、预字符串化 3、缓存事件处理函数 4、Block Tree 5、PatchFlag 一、静态提升 在vue3中的app.vue文件如下: 在服务器中,template中的内容会变异成render渲染函数。 最终编译后的文件: 1.静态节点优化 那么这里为什么是两部分…

内网安全管理系统(保密管理系统)

在当今信息化的时代,企业的内网已经成为其核心资产的重要组成部分。 随着企业的快速发展和信息化程度的提升,内网安全问题日益凸显,如何保障内网的安全和机密信息的保密性,已经成为企业亟待解决的问题。 内网安全管理系统(保密管…

现在的小年轻真的卷得过分了,真是完全不把自己当人啊

现在的小年轻真的卷得过分了,真是完全不把自己当人啊 都说00后躺平了,但是有一说一,该卷的还是卷。这不,前段时间我们公司来了个00后,工作都没两年,跳槽到我们公司起薪18K,都快接近我了。后来才…

常用电子器件学习——MOS管

MOS管介绍 MOS,是MOSFET的缩写。MOSFET 金属-氧化物半导体场效应晶体管,简称金氧半场效晶体管(Metal-Oxide-Semiconductor Field-Effect Transistor, MOSFET)。 一般是金属(metal)—氧化物(oxide)—半导体(semiconductor)场效应晶…

RabbitMQ消息应答与发布

消息应答 RabbitMQ一旦向消费者发送了一个消息,便立即将该消息,标记为删除. 消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个很长的任务并仅仅执行了一半就突然挂掉了,在这种情况下,我们将丢失正在处理的消息,后续给消费者发送的消息也就无法接收到了. 为了…

OpenHarmony驱动消息机制管理

驱动消息机制管理 当用户态应用和内核态驱动需要交互时,可以使用HDF框架的消息机制来实现。 消息机制的功能主要有以下两种: 用户态应用发送消息到驱动。 用户态应用接收驱动主动上报事件。 配置管理 HCS(HDF Configuration Source&…

深入分析 Linux 网络丢包问题

热门IT课程【视频教程】-华为/思科/红帽/oraclehttps://xmws-it.blog.csdn.net/article/details/134398330 所谓丢包,是指在网络数据的收发过程中,由于种种原因,数据包还没传输到应用程序中,就被丢弃了。这些被丢弃包的数量&#…

C++进阶:多态(下)

1、多态的原理 多态之所以可以实现,主要是因为虚函数表的存在,虚函数表用于记录虚函数的地址,他是一个函数指针数组,在类中用一个函数指针数组指针来指向数组,子类继承了父类的虚函数表,当有重写的情况发生…