数据结构--图解单链表

学习链表最重要的就是会画图,尤其是要理解链表的逻辑结构和物理结构,理解链表的底层原理才能使用的如鱼得水。 希望这篇文章可以帮助各位,记得关注收藏哦;若发现问题希望私信博主,十分感谢。

当然学习链表是需要大家对指针和结构体能够较为熟练的使用,尤其是指针,需要能够理解一级指针和二级指针,所以如果大家对指针不够熟练的话,可以去看一下博主的文章。

链表的概念及结构

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

链表的图形化解释

单链表的物理结构

从物理结构中我们可以看出,头节点plist存储的是下一个节点的地址,而下一个节点也通过一种结构去存储了它下一个节点的地址,以此类推,虽然在物理存储结构上是非连续,非顺序的存储方式,但是可以通过其存储的地址精准找到一下个节点,从而得到了整个链表。 

从物理结构中可以看出,链表分为两个部分

  单链表的逻辑结构

我们可以说链表在逻辑结构上是连续的,但是在现实的物理结构中,是没有箭头这种链接形式,箭头在逻辑结构中主要是为了理解更加方便,从逻辑结构上可以更加清晰的发现,链表之间的链接是通过上一个节点存储的地址去找到下一个节点。

单链表的初始化

从物理结构中可以发现,单链表需要使用到两种数据,一个是Data,类型是根据自己要存储的类型随时改变的,例如int或者float等等。还有一个就是指针类型的数据,用来存储下一个节点位置。

标准的初始化        

typedef int SLTDateType;
typedef struct SLTNode
{SLTDateType data;struct SLTNode* next;
}SLTNode;
  1.  在之前的文章中,我们也经常使用 typedef 去重命名数据类型,这样做的好处就是,当类型发生改变的时候,我们可以直接在头文件中改变一个 int 就行,否则你就要在所有文件中找到 int 去挨个改变了。
  2. struct SLTNode* next  很多同学非常疑惑,为什么这个指针的类型是结构体类型,这就考验到同学们对于指针类型的理解,其实指针类型的确定是与它指向的类型保持一致,我们是使用结构体创建的链表,那么指针指向的下一个节点也必然是一个结构体。

初始化的经典错误 

很多同学在初始化的时候经常会写成这样

typedef int SLTDateType;
typedef struct SLTNode
{SLTDateType data;SLTNode* next;
}SLTNode;

 他的想法就是,我已经将结构体命名为SLTNode了,那么就可以使用SLTNode进行命名了,但是问题在于程序是自上而下运行的,先运行结构体,在运行重命名,所以这样写出来之后,结构体内部不会识别出SLTNode这是个结构体。

接口实现

在学会使用链表之前最重要的是要先学会链表的各种接口是如何实现的,比如头插头删,尾插尾删等等,虽然之后大家都是直接调用接口,但是只有先理解基本的实现原理,才会明白哪种接口效率高,什么场景下适合用哪种接口。

创建新节点

因为之后进行尾插头插或者某一个位置插入的程序中,总要创建新的节点,所以我们可以先写一个创建节点的函数,之后直接使用。

SLTNode* BuyListNode(SLTDateType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("fail in malloc");return;}newnode->data = x;newnode->next = NULL;return newnode;
}

创建过程比较简单,就不多赘述了,如果还是对malloc有问题的同学,请看一下这篇文章 可参考动态内存管理icon-default.png?t=N7T8https://blog.csdn.net/Senyu_nuanshu/article/details/131934727

打印链表 

提前写出链表的打印,接口写完之后马上使用打印去判断一下接口是否写错了

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

这里有一个点需要注意,就是尽量不要直接使用头节点,而是创建一个变量保存头节点的地址,使用这个变量实现代码,因为接口有很多种,不可能每次都使用一种,基本都是各种接口的混合使用,当头节点的地址被改变了就会影响下一个接口的使用了 

cur = cur->next;会在尾插详细解释 

单链表尾插

尾插的逻辑结构

//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);if (*pphead == NULL){*pphead = newnode;}else{SLTNode* cur = *pphead;while (cur->next){cur = cur->next;}cur->next = newnode;}
}
  1.  断言(assret)的意义:接下来看到的接口实现当中,基本上都可以看到断言的存在,那么它存在的意义是什么呢,其实就是一种自我警告,判断一下断言里面的内容是不是空指针,当然,断言不止可以判断指针,别的也可以,详细各位可以去网上搜索一下。在这个断言里面,我们判断的是二级指针pphead是否为空,那么pphead是代表什么的呢?我们知道,一级指针可以代表一个变量或者一个函数的地址,那么二级指针其实就是代表了一级指针的地址,既然有了一级指针为什么呢还要使用二级指针呢?因为接口的本质就是函数之间的调用,而函数的参数又分为形参和实参,如果你使用一级指针传参,又使用一级指针接收,那么接收的一级指针其实是形参,形参的改变不影响实参,而我们插入删除的时候改变的是什么,是一级指针,那你函数内部的改变不影响函数外部,岂不是无用功了。所以我们就需要使用二级指针接收一级指针的传参,让二级指针去改变一级指针,因为二级指针就是一级指针的地址,当我们运行二级指针的时候,他就会自动找到一级指针所在的位置,去改变一级指针了。
  2. cur->next是什么意思:结构体里面有两个变量,next就是其中一个,表示下一个节点的地址,我们让cur = cur->next;其实就是将指针的位置换到了下一个节点中了。
  3. 那这段代码的整体逻辑是什么呢:首先第一步就是断言,要判断一下二级指针是不是空指针,因为它存储的是一级指针的地址,它要是空指针,说明开始就是错误的,没存储上,那接下来的所有都无法运行;然后我们要判断一下这个链表是不是空链表,各位要想明白,空链表是可以插入的,如果是空链表,其实尾插就变成头插了,尾插正常逻辑应该是先找到链表的最后一个节点,如何找呢,链表的最后一个节点的特征就是它的next是NULL,如果是空链表,压根就没有next,那按照正常逻辑肯定找不到,所以要先排除这个情况;接下来就是找尾了,同样道理,不要轻易动用头节点,找到尾之后将尾节点next变成newnode的地址。

单链表尾插的经典错误

//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);if (*pphead == NULL){*pphead = newnode;}else{SLTNode* cur = *pphead;while (cur){cur = cur->next;}cur= newnode;}
}

想要了解这个经典的错误,就要理解单链表的物理结构

尾插的物理结构

首先内存中链表的物理结构是没有箭头的,正常是下面的样子

 

当上面的代码刚开始运行的时候,在物理结构上是下面的样子;cur存储了头节点的地址,接下来的  cur=cur->next  使得cur不断更新,存储的一直都是下一个节点的地址。

当不断更新之后,当cur的最终结果NULL的时候,跳出循环;此时cur存储的是4这个节点的next

运行cur = newnode;那么就变成下面的情况

 

从这图中我们可以看出来,cur是逻辑结构里面的连接线吗,其实不是对吧,他就是一个临时变量来存储地址的, cur = newnode;cur确实存储了newnode的地址,但是cur是临时变量,除了函数就被销毁了,4这个节点就很尴尬啊,4想着我把我的地址给你了啊,你怎么还拿着newnode的地址跑了呢。其实尾插的本质是什么,就是将原尾节点中要存储新的尾节点的地址。那么cur的作用应该是什么,通俗一点就是更新链表的作用,4的地址给了cur,cur也判断出这是个尾节点了,那么就应该将4的next变成newnode的地址,就是cur->next = newnode;同时还有一处错误,就是判断条件啊,我们要判断到4这个节点就应该出来了,要是用cur去作为判断条件,最终cur就变成4的next了,它应该是一个桥梁作用,指引着4去找到newnode,不是自己变成newnode。

单链表头插

从图中其实可以看出,头插其实很简单,就是把节点之间的关系换一下 

//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDateType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);newnode->next = *pphead;*pphead = newnode;
}

 

单链表尾删

链表的空间是使用malloc函数开辟出来的,所以在删除的时候就要多考虑一些,而且链表里面都是指针,一旦没有将删除的指针置空,就会导致野指针的出现。

//单链表尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);//链表中只有一个元素的情况if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//链表中的元素大于1个的时候else{SLTNode* cur = *pphead;while (cur->next->next){cur = cur->next;}free(cur->next);cur->next = NULL;}
}
  1. 从代码中我们可以看出来,我们要考虑一种特殊情况,就是链表中只有一个元素,因为删完这个元素之后,就剩下头节点了,这个时候头节点如果不置空,就是导致其变成野指针。
  2. 如果是大于一个元素,那么尾删还是要先找到尾巴,然后在删除,使用free将空间还给操作系统,最重要的是将其置空。
  3. 断言:尾删为什么要断言*pphead,因为单链表为空的时候是不能删除的,啥都没有删除什么,所以断言的用法要灵活,不要找规律,而是要理解。

单链表头删

//单链表头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead);SLTNode* prev = *pphead;*pphead = (*pphead)->next;free(prev);prev = NULL;
}

单链表节点查找

//单链表结点查找
SLTNode* SLTNodeFind(SLTNode* phead, SLTDateType x)
{SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

指针问题:不是所有的接口实现都需要用到二级指针,只有要修改链表的时候才需要用到,像查找使用一级指针就足够了

单链表结点插入(在pos之前插入)

首先解释一下pos是什么,pos就是单链表节点查找的时候返回的节点位置。

  1. 从图中可以发现,从pos之前插入是比较麻烦的,因为你要找到pos的位置,但是你插入的时候还需要pos之前的节点位置才能正常插入 ,所以这个时候就非常考验大家对链表的理解
  2. 当然还要分成两种情况去考虑,如果pos就是第一个节点,那就变成头插了

//单链表结点插入(在pos之前插入)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{assert(pphead);assert(pos);SLTNode* newnode = BuyListNode(x);//头插if (*pphead == pos){newnode->next = pos;*pphead = newnode;}//非头插else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}}

空链表可以插入,但是要确保pos的位置是存在链表当中的,其实就是pos不能为NULL,因为我们是使用自己写的查找程序去寻找,当找不到的时候就会返回NULL;

 单链表结点插入(在pos之后插入) 

void SLTInsertBack(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{assert(pos);assert(pphead);SLTNode* newnode = BuyListNode(x);newnode->next = pos->next;pos->next = newnode;
}

最后赋值的顺序千万不能写错,因为要是先写pos->next = newnode;就会将pos->next节点改变了,那再写newnode->next = pos->next,newnode->next其实就变成newnode自己了。

单链表结点删除(删除pos位置的结点) 

  1. 头节点就是pos
  2. 头节点不是pos

头节点如果是pos,那其实就是头删了,但是如果头节点不是pos,就要注意一个问题,就是删除之后,pos前面的节点与后面的节点之间的链接问题。

删除pos 

从图中就可以非常清晰的看出如何在删除之后处理节点,其实就是要找到pos的前一个节点,找到它就可以找到后面的节点了 

//单链表结点删除(删除pos位置的结点)
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(*pphead);assert(pos);if (*pphead == pos){free(*pphead);*pphead = NULL;}else{SLTNode* plist = *pphead;while (plist->next != pos){plist = plist->next;assert(plist->next);}plist->next = plist->next->next;free(pos);}
}

销毁单链表

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

销毁链表,不能直接free(phead),因为链表在物理结构上是不连续存储的,销毁链表必须要一个结点一个结点去销毁!!!!最后不要忘记把phead置为NULL。

总代码

头文件

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLTDateType;
typedef struct SLTNode
{SLTDateType data;struct SLTNode* next;
}SLTNode;
//创建一个结点
SLTNode* BuyListNode(SLTDateType x);
//销毁单链表
void SLTDestory(SLTNode** pphead);
//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDateType x);
//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x);
//单链表头删
void SLTPopFront(SLTNode** pphead);
//单链表尾删
void SLTPopBack(SLTNode** pphead);
//单链表结点查找
SLTNode* SLTNodeFind(SLTNode* phead, SLTDateType x);
//单链表结点删除(删除pos位置的结点)
void SLTErase(SLTNode** pphead, SLTNode* pos);
//单链表结点插入(在pos之前插入)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);
// 单链表结点插入(在pos之后插入)
void SLTInsertBack(SLTNode** pphead, SLTNode* pos, SLTDateType x);
//打印单链表
void PrintSLT(SLTNode* phead);

节点代码

#define _CRT_SECURE_NO_WARNINGS 1
#include"SL-list.h"void PrintSLT(SLTNode* phead)
{SLTNode* cur = phead;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL");printf("\n");
}//创建一个结点
SLTNode* BuyListNode(SLTDateType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("fail in malloc");return;}newnode->data = x;newnode->next = NULL;return newnode;
}//单链表尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);if (*pphead == NULL){*pphead = newnode;}else{SLTNode* cur = *pphead;while (cur->next){cur = cur->next;}cur->next = newnode;}
}//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDateType x)
{assert(pphead);SLTNode* newnode = BuyListNode(x);SLTNode* cur = *pphead;newnode->next = cur->next;cur = newnode;
}
//单链表头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);assert(*pphead);SLTNode* prev = *pphead;*pphead = (*pphead)->next;free(prev);prev = NULL;
}
//单链表尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);//链表中只有一个元素的情况if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//链表中的元素大于1个的时候else{SLTNode* cur = *pphead;while (cur->next->next){cur = cur->next;}free(cur->next);cur->next = NULL;}
}//单链表结点查找
SLTNode* SLTNodeFind(SLTNode* phead, SLTDateType x)
{SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}//单链表结点插入(在pos之前插入)
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{assert(pphead);assert(pos);SLTNode* newnode = BuyListNode(x);//头插if (*pphead == pos){newnode->next = pos;*pphead = newnode;}//非头插else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;assert(prev->next);}newnode->next = pos;prev->next = newnode;}}//单链表结点删除(删除pos位置的结点)
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(*pphead);assert(pos);if (*pphead == pos){free(*pphead);*pphead = NULL;}else{SLTNode* plist = *pphead;while (plist->next != pos){plist = plist->next;assert(plist->next);}plist->next = plist->next->next;free(pos);}
}// 单链表结点插入(在pos之后插入)
void SLTInsertBack(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{assert(pos);assert(pphead);SLTNode* newnode = BuyListNode(x);newnode->next = pos->next;pos->next = newnode;
}//销毁单链表
void SLTDestory(SLTNode** pphead)
{assert(pphead);SLTNode* cur = *pphead;while (cur){SLTNode* next = cur->next;free(cur);cur = next;}*pphead = NULL;
}

测试程序

#define _CRT_SECURE_NO_WARNINGS 1
#include"SL-list.h"void test1()
{SLTNode* SLT = NULL;SLTPushBack(&SLT, 1);SLTPushBack(&SLT, 2);SLTPushBack(&SLT, 3);SLTPushBack(&SLT, 7);SLTPushBack(&SLT, 8);SLTPushBack(&SLT, 9);SLTPushBack(&SLT, 4);SLTPushFront(&SLT, 1);SLTPopBack(&SLT);SLTPopFront(&SLT);SLTNode* ret = SLTNodeFind(SLT, 3);SLTInsert(&SLT, ret, 20);SLTErase(&SLT, ret);PrintSLT(SLT);SLTNode* PHEAD = SLTNodeFind(SLT, 2);printf("%p ", PHEAD);}
int main()
{test1();return 0;
}

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

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

相关文章

《Linux从练气到飞升》No.28 Linux中的线程同步

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的…

sMLP:稀疏全mlp进行高效语言建模

这是一篇2022由纽约州立大学布法罗分校和Meta AI发布的论文&#xff0c;它主要的观点如下&#xff1a; 具有专家混合(MoEs)的稀疏激活mlp在保持计算常数的同时显着提高了模型容量和表达能力。此外gMLP表明&#xff0c;所有mlp都可以在语言建模方面与transformer相匹配&#xf…

人力物力和时间资源有限?守住1个原则,精准覆盖所有兼容性测试!

随着 APP 应用范围越来越广&#xff0c;用户群体越来越大&#xff0c;终端设备的型号也越来越多&#xff0c;移动终端碎片化加剧&#xff0c;使得 APP 兼容性测试成为测试质量保障必须要考虑的环节。 APP 兼容性测试通常会考虑&#xff1a;操作系统、厂家 ROM、屏幕分辨率、网…

Git笔记简化版

起源 Git是目前世界上最先进的分布式版本控制系统。林纳斯-托瓦兹在开发linux系统时有很多人想有一个平台进行版本控制。当时同类型的版本控制软件是BitKeeper&#xff0c;bitKeep是不开源的。当林纳斯团队无法免费使用它时&#xff0c; 林纳斯花费了一个月左右时间就开发出了…

ubuntu 20.04+ORB_SLAM3 安装配库教程

目录 安装ros(如果只是运行ORB-SLAM3&#xff0c;可以跳过安装)0. ros 安装教程1. 安装opencv2. 安装Pangolin3. 安装Eigen34.安装Python & libssl-dev5.安装boost库6.安装ceres库&#xff08;不必须&#xff09;7.安装Sophus库&#xff08;不必须&#xff09;8. 安装g20库…

echarts:graph图表拖拽节点

需求&#xff1a;实现一个可视化编辑器&#xff0c;用户可以添加节点&#xff0c;并对节点进行拖拽编辑等 实现期间碰到很多问题&#xff0c;特意记录下来&#xff0c;留待将来碰到这些问题的同学&#xff0c;省去些解决问题的时间 问题1&#xff1a;节点的data如下&#xff0…

基于单片机的智能考勤机(论文+源码)

1.系统设计 本课题为基于单片机的智能考勤机&#xff0c;其整个系统由STC89C52单片机&#xff0c;RC522 RFID模块&#xff0c;LCD液晶&#xff0c;按键等构成&#xff0c;在功能上&#xff0c;本系统智能考勤机主要应用在校园生活中&#xff0c;用户可以通过按键注销/注销相应的…

ATECLOUD-POWER电源测试系统有什么特点?如何用它测试电源模块?

ATECLOUD-POWER电源测试系统 ATECLOUD-POWER是检测电源性能的自动化测试系统&#xff0c;针对电源模块各类测试项目提供定制方案&#xff0c;指导电源模块的设计和生产&#xff0c;保证电源的质量、稳定性和可靠性。该方案包括软件定制开发以及硬件设备选择两方面&#xff0c;根…

多种格式图片可用的二维码生成技巧,快来学习一下

将图片存入二维码是现在很常见的一种图片展现方式&#xff0c;有效的节省了图片占用内容空间以及获取图片内容的速度&#xff0c;所以现在会有很多人将不同的图片、照片生成二维码展示。如何使用图片二维码生成器来快速生成二维码呢&#xff1f;下面就让小编来给大家分享一下图…

【算法】最短路径——迪杰斯特拉 (Dijkstra) 算法

目录 1.概述2.代码实现2.1.节点类2.2.邻接矩阵存储图2.3.邻接表存储图2.4.测试 3.扩展3.1.只计算一对顶点之间的最短路径3.2.获取起点到其它节点具体经过的节点 4.应用 本文参考&#xff1a; LABULADONG 的算法网站 1.概述 &#xff08;1&#xff09;在图论中&#xff0c;最短…

应用架构的演进 I 使用无服务器保证数据一致性

在微服务架构中&#xff0c;一个业务操作往往需要跨多个服务协作完成&#xff0c;包含了读取数据和更新多个服务的数据同时进行。在数据读取和写入的过程中&#xff0c;有一个服务失败了&#xff0c;势必会造成同进程其他服务数据不一致的问题。 亚马逊云科技开发者社区为开发者…

2024年软件测试知识应运趋势

每一年&#xff0c;IT互联网技术都在变&#xff0c;那2024年&#xff0c;需要具备哪些知识&#xff0c;才能让我们在软件测试行业里混得风生水起呢&#xff1f; 我认为有以下十点&#xff1a; 1、Linux必备知识 Linux作为现在最流行的软件环境系统&#xff0c;一定需要掌握&am…