【数据结构】单链表(一)

上一篇【数据结构】顺序表-CSDN博客  我们了解了顺序表,但是呢顺序表涉及到了一些问题,比如,中间/头部的插入/删除,时间复杂度为O(N);增容申请空间、拷贝、释放旧空间会有不小的消耗;增容所浪费的空间... 我们如何去解决这些问题?本篇介绍另一个数据结构——链表

1. 链表的概念及结构

链表:是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的,链表也是线性表的一种

链表大概是什么样的呢?看下图

链表是由一个一个的节点组成,再顺序表中,数据是连续存放的,我们想找到下一个数据顺着前一个数据找就行了,而链表中数据存放空间是不连续的,所以我们就需要一个指针来保存下一个节点的地址,所以,我们如果要定义链表,只需要定义链表的节点结构就行了

(在【C语言】结构体详解-CSDN博客 的1.3 结构体的自引用 中介绍过)

struct SListNode
{int data;//数据struct SListNode* next;//下一个数据的地址
};

和上一篇一样,创建一个头文件,两个源文件

 也是一样,在头文件SList.h中定义单链表的结构,并对类型和结构体改名

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int Type; 
//定义节点结构
typedef struct SListNode
{Type data;struct SListNode* next;
}SLNode;

我们先创建几个节点,在test.c中实现

#include "SList.h" //包含头文件
void SListtest1()
{SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));//创建节点1node1->data = 1;//赋值SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));//创建节点2node2->data = 2;//赋值SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));//创建节点3node3->data = 3;//赋值SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));//创建节点4node4->data = 4;//赋值
}
int main()
{SListtest1();return 0;
}

虽然我们创建好了节点,但是这几个节点现在还不能互相找到,所以我们接下来要将这几个节点连接起来,怎么连接?存下一个节点的地址

void SListtest1()
{SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));//创建节点1node1->data = 1;//赋值SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));//创建节点2node2->data = 2;//赋值SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));//创建节点3node3->data = 3;//赋值SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));//创建节点4node4->data = 4;//赋值//将四个节点连接起来node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;
}

现在就构成了一个链表,有了这个简单的链表,我们来写一个函数打印一下里面的数据

链表的打印

SList.h中进行函数的声明

void SLPrint(SLNode* ps);//打印

参数是链表的节点的地址 

SList.c中进行函数的实现

#include "SList.h"
void SLPrint(SLNode* ps) //打印
{SLNode* pcur = ps;//pcur指向当前链表的第一个节点
}

要打印当然就要循环遍历,写一个while循环,判断条件为pcur,不为NULL进入循环 

void SLPrint(SLNode* ps) //打印
{SLNode* pcur = ps;//pcur指向当前链表的第一个节点while (pcur) //判断第一个节点是否为NULL{//....}
}

 进入循环后打印内容

void SLPrint(SLNode* ps) //打印
{SLNode* pcur = ps;//pcur指向当前链表的第一个节点while (pcur){printf("%d->", pcur->data);//打印内容}
}

打印完一个数据后,要把下一个节点值部分的地址给pcur ,也就是pcur要存node2中2的地址,此时pcur不再指向node1,指向了node2

void SLPrint(SLNode* ps) //打印
{SLNode* pcur = ps;//pcur指向当前链表的第一个节点while (pcur){printf("%d->", pcur->data);//打印内容pcur = pcur->next;//指向下一个节点}printf("NULL\n");
}

test.c中测试

测试时我们不直接传链表的第一个节点node1,而是再定义一个结构体指针plist去指向node1,让plist作为参数传过去

    SLNode* plist = node1;SLPrint(plist);

虽然有点多此一举,但这样会让我们的代码更完善 

运行结果

 2.链表的插入

实际使用链表的时候我们不会像上面那样一个一个创建,初始时候是一个空链表,会跟顺序表类似进行空间开辟然后插入数据

2.1空间申请创建节点

插入数据前依旧是要判断空间够不够

SList.h中进行函数的声明

SLNode* SLBuyNode(Type x);//申请空间创建节点

返回值是节点的地址,参数就是要插入的数据

SList.c中进行函数的实现

SLNode* SLBuyNode(Type x)//申请空间创建节点
{SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));if (newnode == NULL) //空间申请失败{perror("malloc");return 1;}newnode->data = x;newnode->next = NULL;return newnode;
}

2.2 尾插

SList.h中进行函数的声明

void SLPushBack(SLNode** ps, Type x);//尾插

参数是链表的节点地址,是一个二级指针,和要插入的数据

我们要给函数传地址,这样形参的改变才能影响实参,对参数的理解如下,这很重要,这里提前展示将要在test.c中测试用的部分代码 

SList.c中进行函数的实现

尾插要先找到尾节点,再将尾节点和新节点连接起来 ,此时node4的地址部分就不再是NULL 而是新节点的地址

代码如何实现呢 ?先找尾节点

void SLPushBack(SLNode* ps, Type x)//尾插
{SLNode* ptail = ps;//定义尾节点,开始时指向头节点while (ptail->next != NULL)//遍历链表数据,找尾节点{ptail = ptail->next;}//跳出循环后ptail指向尾节点
}

然后我们直接调用创建节点的函数,放在找尾节点前面,最后赋值

void SLPushBack(SLNode* ps, Type x)//尾插
{SLNode* newnode = SLBuyNode(x);//申请空间创建节点SLNode* ptail = ps;//定义尾节点,开始时指向头节点while (ptail->next != NULL)//遍历链表数据,找尾节点{ptail = ptail->next;}//跳出循环后ptail指向尾节点ptail->next = newnode;//直接赋值
}

 但是呢,链表在没有赋值的时候是空链表,所以我们要讨论空链表的情况,如果是空链表,直接让ps指向新节点newnode,所以最终的代码如下

void SLPushBack(SLNode** pps, Type x)//尾插
{assert(pps);//pps不可以为NULLSLNode* newnode = SLBuyNode(x);//申请空间创建节点if (*pps == NULL) //*pps可以为NULL,表示空链表{*pps = newnode;}else //非空链表{SLNode* ptail = *pps;//定义尾节点,开始时指向头节点while (ptail->next)//遍历链表数据,找尾节点{ptail = ptail->next;}//跳出循环后ptail指向尾节点ptail->next = newnode;//直接赋值}
}

我们再来看test.c中测试的代码

void SListtest2()
{SLNode* plist = NULL;//空链表SLPushBack(&plist, 1);//尾插SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLPrint(plist);//打印
}
int main()
{//SListtest1();SListtest2();return 0;
}

多插入几个数据,运行看结果,插入成功

2.3 头插

 在SList.h中进行函数的声明

void SLPushHead(SLNode** pps, Type x);//头插

同样的,参数也是一个二级指针,还有要插入的数据

SList.c中进行函数的实现

我们需要做两件事,一个是将newnode和原本的首节点连接在一起,另一件事是*pps要指向新的首节点 

 链表不为NULL时,代码如下

void SLPushHead(SLNode** pps, Type x)//头插
{assert(pps);//pps不能为NULLSLNode* newnode = SLBuyNode(x);//申请空间创建节点newnode->next = *pps;*pps = newnode;
}

当链表为NULL时,分析一下

当前代码也可以实现

test.c中测试

void SListtest2()
{SLNode* plist = NULL;//空链表SLPushBack(&plist, 1);//尾插SLPushBack(&plist, 2);SLPushBack(&plist, 3);SLPushBack(&plist, 4);SLPrint(plist);//打印SLPushHead(&plist, 6);//头插SLPushHead(&plist, 7);SLPushHead(&plist, 8);SLPrint(plist);//打印
}

看一下运行结果

3.链表的删除

删除数据,链表就不能为空,不然删啥呢?

 3.1 尾删

SList.h中进行函数的声明

void SLPopBack(SLNode** pps);//尾删

SList.c中进行函数的实现

很显然,首先就是找尾节点,找到尾节点之后直接释放吗?node4释放后node3->next里面依然存放着node4的地址,但此时指向的空间已经没了,此时的指针就成了一个野指针 ,所以我们还要将被释放节点的前一个节点的指针置空

所以我们还要找尾节点的前一个节点

void SLPopBack(SLNode** pps)//尾删
{assert(pps && *pps);//pps和链表都不能为空SLNode* prev = *pps;//定义尾节点前一个节点SLNode* ptail = *pps;//定义尾节点while (ptail->next){prev = ptail;ptail = ptail->next;}//此时找到了尾节点和尾节点前一个节点free(ptail);//释放尾节点ptail = NULL;//置空prev->next = NULL;//置空尾节点前一个节点
}

如果这个链表只有一个节点呢,这个代码可行吗?来分析一下

我们把节点释放并置空后,prev->next就不存在了,函数最后一句代码就不能实现,所以,当链表里只有一个节点时,直接释放就行了

void SLPopBack(SLNode** pps)//尾删
{assert(pps && *pps);//pps和链表都不能为空if ((*pps)->next == NULL)//链表只有一个节点{free(*pps);//释放节点*pps = NULL;//置空}else //链表不止一个节点{SLNode* prev = *pps;//定义尾节点前一个节点SLNode* ptail = *pps;//定义尾节点while (ptail->next){prev = ptail;ptail = ptail->next;}//此时找到了尾节点和尾节点前一个节点free(ptail);//释放尾节点ptail = NULL;//置空prev->next = NULL;//置空尾节点前一个节点}
}

test.c中测试

void SListtest2()
{SLNode* plist = NULL;//空链表SLPushBack(&plist, 1);//尾插SLPushBack(&plist, 2);SLPrint(plist);//打印SLPushHead(&plist, 6);//头插SLPushHead(&plist, 7);SLPrint(plist);//打印SLPopBack(&plist);//尾删SLPrint(plist);//打印
}

看下结果,尾删成功

3.2 头删

SList.h中进行函数的声明

void SLPopHead(SLNode** pps);//头删

SList.c中进行函数的实现

我们要删除头节点,跟删除尾节点不一样,如果我们把首节点释放掉,还能找到首节点里的next吗?不能,不能的话就找不到第二个节点以及剩下的节点

我们应该先把下一个节点的信息存储起来

SLNode* Next = (*pps)->next;//存节点

 然后把原头节点释放

free(*pps);

最后让*pps指向新的头节点

*pps = Next;

所以代码如下

void SLPopHead(SLNode** pps)//头删
{assert(pps && *pps);//pps和链表都不能为空SLNode* Next = (*pps)->next;//存节点free(*pps);*pps = Next;
}

当链表只有一个节点时,分析一下,上面的代码也是可以完成的

我们在test.c中测试一下

void SListtest2()
{SLNode* plist = NULL;//空链表SLPushBack(&plist, 1);//尾插SLPushBack(&plist, 2);SLPrint(plist);//打印SLPushHead(&plist, 6);//头插SLPushHead(&plist, 7);SLPrint(plist);//打印SLPopBack(&plist);//尾删SLPrint(plist);//打印SLPopHead(&plist);//头删SLPrint(plist);//打印
}

下篇我们再继续介绍更多内容,拜拜~

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

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

相关文章

蓝桥杯刷题 二分-[99]分巧克力(C++)

题目描述 儿童节那天有 K 位小朋友到小明家做客。 小明拿出了珍藏的巧克力招待小朋友们。 小明一共有 N 块巧克力&#xff0c;其中第 i 块是 HiWi 的方格组成的长方形。 为了公平起见&#xff0c;小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。 切出的巧克力需要…

java数据结构与算法刷题-----LeetCode210. 课程表 II

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 文章目录 深度优先遍历但不进行逆拓扑排序&#xff08;不用栈&#xff09;…

leetcode 343. 整数拆分

题目 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 示例 1: 输入: n 2 输出: 1 解释: 2 1 1, 1 1 1。 示例 2: 输入: n 10 输出: 36 解释: 1…

数据结构与算法 — 贪心算法

数据结构与算法 数据结构与算法是计算机科学中的两个核心概念&#xff0c;它们在软件开发和问题解决中起着至关重要的作用。 数据结构 数据结构是计算机中存储、组织和管理数据的方式&#xff0c;它能够帮助我们高效地访问和修改数据。不同的数据结构适用于不同类型的应用场…

记一次IP访问MySQL失败多次被自动锁定导致无法连接问题,解决方法只需一条SQL。

&#x1f469;&#x1f3fd;‍&#x1f4bb;个人主页&#xff1a;阿木木AEcru &#x1f525; 系列专栏&#xff1a;《Docker容器化部署系列》 《Java每日面筋》 &#x1f4b9;每一次技术突破&#xff0c;都是对自我能力的挑战和超越。 前言 今天下午还在带着耳机摸鱼&#xff…

获取当前的年、月、日、时、分、秒,并将这些信息用作保存 Excel 文件的前缀

要获取当前的年、月、日、时、分、秒&#xff0c;并将这些信息用作保存 Excel 文件的前缀&#xff0c;你可以使用 Python 的 datetime 模块来获取当前时间&#xff0c;并格式化时间字符串&#xff0c;然后使用 pandas 库将数据保存为 Excel 文件。示例代码&#xff1a; from d…

Netty的基本架构与组件

Netty实战精髓 前言 Netty的组成部分 1、Channel 2、Callback 3、Future ChannelFuture 提供多个附件方法来允许一个或者多个 ChannelFutureListener 实例&#xff0c;这个回调方法 operationComplete() 会在操作完成时调用。 4、Event和Handler 5、EventLOOP Netty 通过触发…

Objective-C学习笔记(block,协议)4.10

1.block&#xff1a;是一个数据类型&#xff0c;存储一段代码&#xff0c;代码可以有参数有返回值。 2.声明block&#xff1a; 返回值类型 (^block变量名称)(参数列表); int (^myblock) (int num1,int num2); 代码段格式&#xff1a;^返回值类型(参数列表){ 代码段 }; int (^m…

如何在极狐GitLab 启用依赖代理功能

本文作者&#xff1a;徐晓伟 GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 本文主要讲述了如何在[极狐GitLab…

UG NX二次开发(C#)-使用UG NX的开发模板创建项目失败的原因和解决方案

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 1、前言2、可能的原因与解决方案2.1 Visual studio安装不完整2.2 解决方案2.2.1 对于 .net 桌面开发2.2.2 使用C++的桌面开发2.2.3 通用Windows平台开发2.2.4 visual studio扩展开发2.2.5 单个组件…

C语言 获取系统时间

获取系统时间的函数 #define _TIMEBUFLEN (40) // 获取日志缓冲需要的大小 void GetSysTime(char* pTimeBuf, u32 dwSize) {if (pTimeBuf NULL){return;} #if defined(_MSC_VER)SYSTEMTIME sys { 0 };GetLocalTime(&sys);sprintf_s(pTimeBuf, dwSize, "%4d-%02d-%0…

SSH穿透ECS访问内网RDS数据库

处于安全考虑&#xff0c;RDS一般只会允许指定的IP进行访问&#xff0c;而我们开发环境的IP往往是动态的&#xff0c;每次IP变动都需要去修改RDS的白名单&#xff0c;为我们的工作带来很大的不便。 那么如何去解决这个问题&#xff1f; 假如我们有一台ESC服务器&#xff0c;E…

STC89C52学习笔记(八)

STC89C52学习笔记&#xff08;八&#xff09; 综述&#xff1a;本文讲述了LED点阵屏以及如何进行数据串行输入&#xff0c;并行输出。 一、LED点阵屏 1.介绍 LED点阵屏由多个LED组成&#xff0c;以矩阵形式排列&#xff08;类似于矩阵键盘&#xff09;&#xff0c;像素一般…

Objective-C学习笔记(@property,id,instancetype)4.9

1.增强property&#xff1a;自动生成getter和setter方法的声明和实现。 原理&#xff1a;①自动声明私有属性②自动生成getter和setter方法的声明和实现。 property 数据类型 变量名 注意变量名要去掉下划线&#xff0c;如果同时重写getter和setter那么不回自动声明私有属性。…

【iOS ARKit】App 中嵌入 AR Quick Look

AR Quick Look 功能强大&#xff0c;但在应用中嵌入并使用它实现 AR体验却非常简单&#xff0c;如其他所有QuickLook使用一样&#xff0c;简单到只需要提供一个文件名就可以达到目标。 AR Quick Look 支持.usdz 和.reality 两种格式文件&#xff0c;如果在 Xcode 工程中引入了 …

CSS滚动条样式修改

前言 目前我们可以通过 CSS伪类 来实现滚动条的样式修改&#xff0c;以下为修改滚动条样式用到的CSS伪类&#xff1a; ::-webkit-scrollbar — 整个滚动条 ::-webkit-scrollbar-button — 滚动条上的按钮 (上下箭头) ::-webkit-scrollbar-thumb — 滚动条上的滚动滑块 ::-web…

安卓逆向之-Xposed RPC

引言: 逆向为最终的协议,或者爬虫的作用。 有几种方式,比如直接能力强,搞成协议。 现在好多加密解密都写入到so ,所以可以使用unidbg 一个可以模拟器so 执行的环境的开源项目。RPC 调用,又分为Frida, 还有今天讲的Xposed RPC。 原理: Xposed 可以hook ,然后可以直接…

了解大语言模型的参数高效微调(Parameter-Effcient Fine-Tuning)

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 大语言模型在众多应用领域实现了突破性的进步&#xff0c;显著提升了各种任务的完成度。然而&#xff0c;其庞大的规模也带来了高昂的计算成本。这些模型往往包含数十亿甚至上千亿参数&#xff0c;需要…

Golang使用PGO优化程序性能

文章目录 参考文章PGO是什么使用PGO的好处PGO做了什么热函数内联什么是内联内联的好处Go默认的内联策略查看内联预算PGO的热函数内联 去虚拟化调用指令高速缓存 PGO有什么缺点可执行程序变大构建时间变长 PGO怎么使用典型的工作流程收集CPU配置文件生产环境启动PGO代码改动重新…

html中部分重要或有趣的标签讲解

1.a标签 在 HTML 语法中&#xff0c;a 标签称作 锚元素&#xff0c;这种称呼来源于 a 标签这种元素具有锚定资源的功能。从归属来看&#xff0c;所锚定的资源可以本地的&#xff0c;也可是来自网络上的&#xff1b;而从分类来看&#xff0c;所锚定的资源&#xff0c;可以是网页…