【数据结构】链表及无头单向非循环链表实现

目录

1.顺序表的问题

2.链表的概念、结构及分类

3.无头+单向+非循环链表实现

3.1创建节点

3.2头插数据 

3.3头删数据

3.4尾插

3.5尾删

3.6链表销毁

3.7查找一个元素 

3.8在pos之前插入 

3.9在pos之后插入

3.10删除pos位置

3.11删除pos之后的位置


1.顺序表的问题

顺序表的缺点:

  1. 中间和头部插入数据的时间复杂度为O(N)
  2. 增容需要申请空间,realloc函数可能会进行异地扩容,拷贝数据并释放旧空间存在消耗
  3. 增容一般是呈两倍的增长,势必会有一部分空间的浪费

顺序表问题的改进:链表

对于顺序表,其在物理内存上的存储是连续的,而链表通过指针访问,物理存储不一定连续,并且链表结构的节点可以按需申请和释放

2.链表的概念、结构及分类

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

📖Note:

  1. 由上图,链式结构在逻辑结构上是连续的,但在物理结构上不一定连续
  2. 一般情况下节点都是在堆区申请的
  3. 从堆区申请空间,两次申请的空间可能连续,也可能不连续

链表分类:

1️⃣单向或者双向

2️⃣带头或者不带头

3️⃣循环或者非循环:

以上三类排列组合可以形成8种不同类型的链表

实际中最常用的两种为:无头单向非循环链表和带头双向循环链表

🔅无头单向非循环链表:结构简单,一般不会用来单独存储数据。实际中更多是作为其他数据结构的子结构,,如哈希桶,图的邻接表等

🔅带头双向循环链表:结构最复杂,一般单独存储数据,。实际中使用的链表数据结构,都是带头双向循环链表。虽然该结构复杂,但使用代码实现却更简单

3.无头+单向+非循环链表实现

首先创建一个节点结构:

typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode,*PSLTNode;
//PSLTnode是一个结构体指针,指向下一个节点

📖Note :

单链表不需要初始化,可以直接定义一个空链表

SLTNode* plist = NULL;//定义一个空链表

3.1创建节点

由于创建新节点的操作在接下来的函数中也会进行,并且在函数中创建的节点为局部节点,出了作用域就会销毁,所以可以将创建节点操作封装成函数

//创建一个节点
SLTNode* BuySLTNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail");exit(-1);//退出}newnode->data = x;newnode->next = NULL;return newnode;
}

创建的节点如下:

📖Note :

这是一个独立的节点,此时与链表不存在任何联系

新节点与链表建立联系即插入数据的过程

3.2头插数据 

头插数据,我们需要创建一个新节点再进行头插

对于指向链表头的指针phead,我们需要让它指向新的链表头 

即需要改变phead中存放的地址,因为形参只是实参的临时拷贝,所以在函数中传值调用改变形参的值并不会影响实参,所以对phead需要传址调用;phead是一个指针类型,指向phead的指针是一个二级指针

单链表存在两种状态 :空链表和非空链表

对链表的操作都应该分情况讨论

1️⃣对于非空链表,头部插入数据的步骤如下

2️⃣对于空链表,头插数据的步骤如下:

可以发现,空链表和非空链表的头插步骤是相同的,因此可以归并为一类

头插函数SListPushFront的参数问题:

头插数据,我们需要改变phead,phead是一个结构体指针,指向第一个节点,当我们向头插函数传入参数phead后,我们在头插函数中完成了头插操作,但形参只是实参的一份临时拷贝,我们在头插函数中对形参的修改并不会影响实参的值,即头插操作出了函数实际并没有完成,如下图解释

函数完成头插操作如上图,当头插函数调用结束,其创建的函数栈帧也随之销毁,plist便不能找到我们要插入的节点,因此头插失败

以上为传值调用的过程,即将phead的值传给函数,但实参和形参之间没有实质性的联系,对形参的改变并不会改变实参,因此我们需要传址调用,即将phead的地址传给头插函数,在头插函数内通过访问phead的地址改变其实际值

phead是一个结构体指针,它的类型是SLTNode*,所以它的地址是一个二级指针,类型是SLTNode**,以下为代码实现

//头插
void SListPushFront(SLTNode** phead, SLTDataType x)
{//创建一个节点SLTNode* newnode = BuySLTNode(x);newnode->next = *phead;*phead = newnode;//头插之后新节点为头节点}

为了便于观察,我们可以封装一个函数来打印链表 

打印链表只是访问数据,不会改变链表中的值,所以phead传值调用即可

//打印
void SListPrint(SLTNode* phead)
{//空链表phead==NULLSLTNode* cur = phead;while (cur != NULL){printf("%d ", cur->data);cur = cur->next;}printf("NULL\n");
}

3.3头删数据

对于指向链表头的指针phead,我们需要让它指向删除原头节点后新的链表头 

头删也需要改变phead中存放的地址,所以形参应为二级指针

空链表和非空链表应该分情况讨论

1️⃣对于非空链表

2️⃣当链表中的所有元素删除完之后,该链表为空链表,空链表不能进行删除操作,所以应该对phead的值进行检查,为空则不能删除,以下代码采用的是暴力检查的方法,链表为空则不能删除并且报错误信息

//头删
void SListPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead);//空链表则不能删除SLTNode* del = *pphead;*pphead = (*pphead)->next;//释放被删除节点所用占用的空间free(del);del = NULL;	
}

3.4尾插

尾插操作首先需要我们找到尾节点的位置,尾节点tail的特征是tail->next=NULL;通过遍历查找尾节点,再将新节点和尾节点链接即可

1️⃣对于非空链表,我们不需要改变头节点指针plist的值,可以理解为非空链表尾插时改变的是结构体成员变量,我们只需要结构体指针就能访问结构体成员变量

2️⃣对于空链表,我们需要改变头节点指针plist的值,可以理解为空链表尾插时要改变结构体指针,只能通过二级指针访问

空链表需要传址调用,非空链表传值调用即可,为了代码的简洁,我们统一采用传址调用

具体实现如下:

//尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);//创建新节点SLTNode* newnode = BuySLTNode(x);//空链表,直接插入if (*pphead == NULL){*pphead = newnode;}//非空链表,遍历找尾节点else{SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}//找到尾节点,插入新节点tail->next = newnode;}
}

3.5尾删

尾删操作也需要我们找到尾节点的位置,尾节点tail的特征是tail->next=NULL;

同时尾删操作需要找到尾节点的前一个节点,并将其指针域置空

因此我们需要两个指针prev和tail进行迭代

1️⃣对于非空链表,尾删步骤如下:

2️⃣对于只有一个节点的链表,尾删只需要直接释放发、该节点,将phead置空

3️⃣对于空链表,则不能进行删除,采用暴力检查即可

实现如下:

//尾删
void SListPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);//链表为空则不能删除//对于只有一个节点的链表if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//对于多于一个节点的链表else{SLTNode* prev = *pphead;SLTNode* tail = *pphead;while (tail->next != NULL){prev = tail;tail = tail->next;}prev->next = NULL;free(tail);tail = NULL;}	
}

3.6链表销毁

我们对链表的操作并不是每次都能将其节点删除完,因此当程序运行结束时,就可能存在内存泄漏问题,我们需要在每次程序退出之前对链表进行销毁,将其节点所占用的内存空间释放,可以将这个操作封装成一个函数

由于链表结构的特殊性,它并不能像顺序表一样一次性释放所有空间,只能每次释放一个节点,所以我们通过遍历依次释放所有节点

//链表销毁
void SListDestroy(SLTNode** pphead)
{assert(pphead);SLTNode* cur = *pphead;while (cur != NULL){SLTNode* next = cur->next;free(cur);cur = next;}*pphead = NULL;
}

3.7查找一个元素 

查找一个元素,需要遍历链表,查找元素并不会改变链表结构,所以传值调用即可

 📖Note

1️⃣当链表为空则不进行查找,暴力检查

2️⃣链表遍历结束,没找到对于元素,则返回空指针NULL

//查找一个元素 
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{assert(phead);//空链表不能进行查找SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}else{cur = cur->next;}}return NULL;
}

3.8在pos之前插入 

给定一个位置pos,在该位置之前插入一个节点

1️⃣对于pos不等于头指针phead的情况,不会改变头指针phead,步骤如下图:

由上图可以看出前插需要pos位置的前一个节点的指针,这样新节点才能与链表建立联系

使用两个指针进行迭代,找到pos节点及其前一个节点 ,链接新节点即可

2️⃣当pos恰好等于头指针phead时,其前一个节点不存在,但这时可以认为这是头插操作,直接调用头插函数即可,此时需要二级指针

所以我们统一使用二级指针

//在pos之前插入
void SListInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySLTNode(x);//创建一个新节点//pos恰好等于头指针pheadif (pos == *pphead){SListPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;//检查pos的正确性,即pos可能不存在于链表中assert(prev);}prev->next = newnode;newnode->next = pos;}	
}

pos位置前插函数需要与查找函数配合使用,从而能给定pos的位置 

    //pos之前插入SLTNode* pos = SListFind(plist, 1);//在1之前插入元素5if (pos){SListInsertBefore(&plist, pos, 5);}SListPrint(plist);

3.9在pos之后插入

在pos之后插入,pos恰好等于头指针phead时插入和pos不等于头指针phead时插入的步骤是相同的,为了保证代码统一性,pos后插函数也使用二级指针

📖Note

上图中是①②的顺序不能调换,如果先执行②,再执行①,newnode指向其本身,插入失败

//在pos之后插入
void SListInsertAfter(SLTNode* phead, SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySLTNode(x);//创建一个新节点newnode->next = pos->next;pos->next = newnode;
}

 pos位置前插函数需要与查找函数配合使用,从而能给定pos的位置 

//pos之后插入SLTNode* pos = SListFind(plist, 4);//元素4之后插入元素5if (pos){SListInsertAfter(&plist, pos, 5);}SListPrint(plist);

3.10删除pos位置

删除pos的位置,需要分类讨论

1️⃣pos不等于头指针phead

 2️⃣pos等于头指针phead,使用二级指针,此时即头删操作,调用头删函数即可

//删除pos位置
void SListErase(SLTNode** pphead, SLTNode* pos)
{assert(pos);//pos等于头指针,相当于头删if (pos == *pphead){SListPopFront(pphead);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;//检查pos的正确性assert(prev);}prev->next = pos->next;free(pos);pos = NULL;}
}

删除pos位置函数需要与查找函数配合使用,从而能给定pos的位置 

//删除pos位置SLTNode* pos = SListFind(plist, 4);if (pos){SListErase(&plist, pos);}SListPrint(plist);

3.11删除pos之后的位置

删除pos之后的位置,pos为尾节点时删除和不是尾节点时删除相同

//删除pos之后的位置
void SListEraseAfter(SLTNode* phead, SLTNode* pos)
{assert(pos);//对于只有一个节点的链表,不能进行后删if (pos->next == NULL){return;}else{SLTNode* next = pos->next;pos->next = next->next;free(next);}	
}

删除pos位置之后元素函数需要与查找函数配合使用,从而能给定pos的位置 

//删除posz之后位置SLTNode* pos = SListFind(plist, 3);if (pos){SListEraseAfter(&plist, pos);}SListPrint(plist);

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

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

相关文章

【Spring | 应用上下文】

应用上下文 应用上下文和资源路径构造应用上下文构造ClassPathXmlApplicationContext实例 — 快捷方式使用通配符蚂蚁式图案类路径:前缀 应用上下文和资源路径 本节介绍如何使用资源创建应用程序上下文,包括使用 XML 的快捷方式、如何使用通配符以及其…

搭建srt服务器

目录 1、下载和编译srt2、下载和编译srt-live-server3、编译工程,并把编程出来的可执行程序放到nfs4、板子上跑程序5、用ffplay从srt服务器上流6、srt中./configure遇到的问题解决方法1、下载源码2、下载好之后cp到ubuntu3、解压安装4、创建软链接:创建快捷名字tclsh,放到us…

信音电子在创业板IPO:募资约9亿元,预计上半年收入约4.3亿元

7月17日,信音电子(中国)股份有限公司(下称“信音电子”,SZ:301329)在深圳证券交易所创业板上市。本次上市,信音电子的发行价为21.00元/股,发行数量为为4300万股,募资总额…

vue 当新增样式无法生效的情况下如何处理

使用scoped属性时&#xff0c;会遇到样式问题。需要使用样式穿透解决 <style lang"scss" scoped> </style> 可以使用以下方法 &#xff1a;deep css 使用 >>> less 使用 /deep/ scss 使用 ::v-deep 代码写法如下: .a :deep(.b) { } .…

Bash 第十行

195 第十行 给定一个文本文件 file.txt&#xff0c;请只打印这个文件中的第十行。 示例: 假设 file.txt 有如下内容&#xff1a; Line 1 Line 2 Line 3 Line 4 Line 5 Line 6 Line 7 Line 8 Line 9 Line 10 你的脚本应当显示第十行&#xff1a; Line 10 来源&#xff1a;…

8年测试总结,App自动化测试-Appium常遇问题+解决(详细整理)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 问题1&#xff1a…

正则表达式与文本处理器

文本处理器三剑客&#xff1a;grep&#xff08;查找&#xff09; sed awk 正则表达式&#xff1a;由一类特殊字符以及文本字符所编写的一种模式&#xff0c;处理文本当中的内容 其中的一些字符不表示字符的字面含义&#xff0c;这些字符表示控制或者通配的功能 通配符&…

拿了 7 个大厂 offer,我有话说

我有一个朋友&#xff0c;举办了一个分享会。他春招期间收割了阿里、京东、美团、拼多多、网易、腾讯云智等多家公司的实习 offer。 他最近从一家A大厂实习换到了另外一家B大厂实习&#xff0c;分享会长达100分钟&#xff0c; 共解答了 40 多个问题。 我也从中抽取一些比较有共…

Ceph 应用(CephFS文件存储、块存储、对象存储)

目录 一&#xff1a;创建 CephFS 文件系统 MDS 接口 1、服务端操作 &#xff08;1&#xff09;在管理节点创建 mds 服务 &#xff08;2&#xff09;查看各个节点的 mds 服务 &#xff08;3&#xff09;创建存储池&#xff0c;启用 ceph 文件系统 &#xff08;4&#xff09;…

【SQL】计算每个人的完成率

目录 前提任务的完成率前三名拓展&#xff1a;达梦如何去实现除法有余数拓展&#xff1a;MySQL 任务的完成率前三名 前提 达梦数据库&#xff1a; select 1/3; # 0不要求四舍五入 任务的完成率前三名 # nick_name 人名 # finishNum 当前这个人的任务完成数 # total 当前这…

跨文化合作:如何解决海外网红营销中的文化差异?

随着社交媒体的快速发展&#xff0c;海外网红营销已成为许多品牌和企业获取国际市场的有效方式。然而&#xff0c;由于不同国家和地区存在着独特的文化差异&#xff0c;如语言、价值观、习俗等&#xff0c;这也给品牌进行海外网红营销带来了一系列挑战。本文Nox聚星将和大家探讨…

Revit中如何添加剖面?快速实现剖面图

一、Revit中如何添加剖面&#xff1f; 除了标高绘制所得到的楼层平面视图和立面视图之外&#xff0c;还可以添加剖面视图&#xff0c;这样可以得到任意位置一个竖向的剖切面&#xff0c;例如在楼梯细节处理中&#xff0c;楼梯处于建筑物内部&#xff0c;立面也看不到整个楼梯的…