单链表-----重置版
创始人
2024-06-02 22:32:07
0

本期我们对之前讲过的单链表进行重置,增加了亿些细节,不过整体变化不大

(13条消息) 链表与其多种接口实现1-CSDN博客

目录

 链表的结构

打印

尾插

创建新节点

头插

尾删

头删

查找

任意位置前插入

任意位置删除

pos后插入

pos后删除

全部代码


我们仍然使用工程化的写法

 链表的结构

typedef int SLTDataType;
typedef struct SListNode {SLTDataType data;struct SListNode* next;
}SLTNode;

非常简单,一个存储数据的类型,一个链表节点的指针类型,用来指向下一个节点

我们用画图的方式来看看链表

我们一般画链表都会这样画,这是链表的逻辑结构,但实际当中,内存里是没有这些箭头什么的,我们来看看链表的物理结构

这是实际上链表的存储方式,大家要这样来理解,才能深刻理解链表到底是什么,这些0x112等等都是地址,链表是靠地址来找下一个节点的 

打印

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

我们先完成打印,依次打印链表的元素,方便我们观察,我们发现,这次我们没有使用断言,为什么?phead需要断言吗?答案是不需要,如果大家看了之前的顺序表的话,会发现顺序表进行了断言,这是为什么?因为顺序表并不是直接存在结构体里的,而是我们在顺序表的结构体里设置一个指针a,指向了一个数组,数组里才存放的是数据,如果顺序表为空,是由他的容量为0来控制的,而不是指针为空,这里的链表为空,是可以的,链表为空的话就是一个null,也是可以打印的,所以我们这里没有进行断言

尾插

void SLTPushBack(SLTNode* phead, SLTDataType x)//尾插
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL) {perror("malloc fail");return;}newnode->data = x;newnode->next = NULL;//找尾if (phead == NULL) {phead = newnode;}else {SLTNode* tail = phead;while (tail->next != NULL) {tail = tail->next;}tail->next = newnode;}
}

我们创建一个新节点,然后将他初始化,接着我们找到原链表的尾节点,将新节点链接在尾节点即可,如果链表本身为空,那我们把新节点就设为头节点

尾插的本质是原尾节点中存储新尾节点的地址,我们来画图演示

 ​​​​​

 看着这张图,大家就可以深刻理解尾插是怎么回事了

 我们对代码进行测试

发现测试结果和我们想的并不一样,这是为什么呢?我们慢慢往下看

我们知道,形参的改变并不影响实参 

我们传x的地址就可以改变x,这是传值和传址的区别

我们来看这样一段代码

 

画在内存里就是这样的,ptr的改变并不会影响px,而且后边还会释放Fun,存在内容泄漏的问题,我们来对比上下几个函数,我们想改变x时,x是int类型,我们传的参数为int*才可以改变x,而int不可以,这里我们想改变px,px为int*类型,那我们传int*类型正确吗?显然是错误的,我们应该传int**才可以改变int*

我们看这段代码

  

我们这次传的是px的地址,所以*pptr就是px, 此时我们申请的空间就可以用px访问了

 此时再回过头来想我们上面的尾插函数,为什么错了呢?因为我们要修改SLTNode*,我们传的却还是SLTNode*,我们需要传入SLTNode**才可以改变SLTNode*

修改后的尾插为

void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL) {perror("malloc fail");return;}newnode->data = x;newnode->next = NULL;//找尾if (*pphead == NULL) {*pphead = newnode;}else {SLTNode* tail = *pphead;while (tail->next != NULL) {tail = tail->next;}tail->next = newnode;}
}

此时就可以正常运行了,这里就可能会有人迷惑,为什么自己在学习时的链表没有使用二级指针呢?这有多种原因,其中一种就可能是你学习的链表是有哨兵位的头节点的,关于哨兵位,循环链表,双向,这些结构之后会出一期专门讲解,这里不再探究

到这里可能会有人发现,我们在找尾时,链表为空使用的是二级指针,而不为空时使用的是一级指针,这是为什么呢? 

大家在这里要想清楚,我们要改变int,要用int*,要改变int*要用int**,我们要改变SLTNode*时就要用SLTNode**,而我们的PushBack函数,我们调用了四次

我们四次都传的是plist的地址,但是其实从第二次开始,我们就用不上plist的地址了,但因为我们调的是同一个函数,所以才会这样(我们总不可能多写一个函数来区分链表为空时的尾插和链表不为空时的尾插吧),只有当plist为空,也就是*pphead为空时,才会使用plist的地址,后边都用不上了,我们拿尾插4时举例

此时的大概就是这样一个情况,我们此时要把tail的next指向newnode,我们这里需要改变的是结构体还是结构体的指针?我们要改变的是结构体,tail是指向这个结构体的,我们有了结构体的指针,是可以改变结构体的,把结构体的next改变,我们现在要改变的是Node,改变的是这个节点,所以我们用的是Node*,大家看看链表的结构体,再画一下图就可以想明白了

此时我们回过头来再看Print函数,需要传二级吗?可以,但没必要,因为我们并没有改变任何东西

创建新节点

我们在写头插之前,发现我们的头插和尾插都需要创建新的节点,所以我们把他提取出来,写成一个函数

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

将尾插的前一段代码提取即可,再修改一下malloc失败的情况,返回NULL即可

头插

void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{SLTNode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}

头插就非常简单,三句搞定,我们让新节点的next指向plist(即*pphead),然后再把新节点变为头节点即可,而且,这段代码也可以处理plist为空的情况

尾删

void SLTPopBack(SLTNode** pphead)//尾删
{SLTNode* prev = NULL;SLTNode* tail = *pphead;while (tail->next != NULL) {prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;
}

尾删我们除了要找到尾节点,还需要找到前一个节点,不然我们释放尾节点的空间后,前一个节点的指向不是NULL,就是野指针了

还可以这样写

void SLTPopBack(SLTNode** pphead)//尾删
{SLTNode* tail = *pphead;while (tail->next->next != NULL) {tail = tail->next;}free(tail->next);tail->next = NULL;
}

当我们进行测试后,其实是会出现问题的,两种写法都有问题,当链表只剩一个节点时,第二种写法首先出错,tail为空时也会出问题,当只剩一个节点时,第一种写法不会进入循环,直接free掉节点,此时最后一句代码,prev->next就会出现问题,所以我们进行修改

void SLTPopBack(SLTNode** pphead)//尾删
{//暴力检查//assert(*pphead != NULL);//温柔的检查if (*pphead == NULL) {return;}//只有一个节点if ((*pphead)->next == NULL) {free(*pphead);*pphead = NULL;}else {//多个节点/*SLTNode* prev = NULL;SLTNode* tail = *pphead;while (tail->next != NULL) {prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;*/SLTNode* tail = *pphead;while (tail->next->next != NULL) {tail = tail->next;}free(tail->next);tail->next = NULL;}
}

我们分情况讨论即可,对于链表为空时,我们进行检查,大家自行选择即可

头删

void SLTPopFront(SLTNode** pphead)//头删
{//严厉的检查//assert(*pphead != NULL);//温柔的检查if (*pphead == NULL) {return;}SLTNode* first = *pphead;*pphead = first->next;free(first);first = NULL;
}

有了尾删的经验,头删就非常简单了

查找

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

我们有时也需要查找数据,所以我们写出查找函数,找到的话返回该节点,否则返回NULL,有了查找我们就可以完成许多事情了

任意位置前插入

void SLTInsert(SLTNode** pphead, SLTNode* pos,SLTDataType x)//pos位置前插入
{assert(pos);assert(pphead);if (pos == *pphead) {SLTPushFront(pphead, x);}else {//找到pos前的位置SLTNode* prev = *pphead;while (prev->next!=pos) {prev = prev->next;}SLTNode* newnode = BuySLTNode(x);prev->next = newnode;newnode->next = pos;}
}

当我们有了查找,就可以和任意位置插入一起使用,我们写出任意位置插入,当pos是头节点时,我们直接头插即可,否则我们找到pos的前一个节点,接着我们创建一个新节点,将新节点插入到pos前即可,另外,我们还需要在开始时进行断言,防止传的pos是不存在的,还要判断一下pphead,我们的链表是plist,pphead是plist的地址,所以pphead绝对不会为空,所以需要进行断言,这里就可以发现,我们之前写的函数很多都是要判断pphead的,我们这里对之前的函数进行补充,尾插尾删头插头删这些需要plist的,pphead都不可能为空,所以都需要加上断言,大家进行补充即可,另外,在尾删时,我们还判断了*pphead不能为空,我们需要把判断pphead放在*pphead之前,否则判断pphead就没有意义(因为已经解引用了),头删同理,大家发现,头插和尾插没有判断*pphead,为什么呢?当链表为空时,我们是可以插入的,而链表为空时,我们是不能删除的,所以不是所有的情况都需要断言,大家要根据实际情况来进行判断

任意位置删除

void SLTErase(SLTNode** pphead, SLTNode* pos)//pos位置删除
{assert(pphead);assert(pos);//assert(*pphead);if (pos == *pphead) {SLTPopFront(pphead);}else {SLTNode* prev = *pphead;while (prev->next != pos) {prev = prev->next;}prev->next = pos->next;free(pos);}
}

接着,我们来完成pos位置的删除,我们先进行断言,这里大家发现,我们没有对*pphead进行断言,这是因为对pos进行断言的话,间接对*pphead进行了断言,如果pos没有传错的话,那么*pphead就绝对不会为空,所以我们这里没有进行断言,不过进行断言也是可以的,没有影响,删除pos位置的函数也非常简单,如果pos是头,我们进行头删即可,否则找到pos之前的位置,将pos之前的节点的next指向pos的next,接着free掉pos即可,这里释放空间后,pos没有置空,是因为形参的改变不影响实参

这里再给各位开拓一下思维,如果我们只给了pos位置,没有给pphead,我们可以在pos前插入吗?答案是可以的,我们先在pos后插入,然后交换pos和这个节点的值即可

如果只给了pos位置,没有给pphead,我们可以进行删除pos位置吗?也是可以的,我们将pos后一个节点的值给pos,然后删掉pos后一个节点即可,不过这个写法是不能删尾的

这里我们发现,单链表在之前插入和某个位置的删除是非常麻烦的,而在某个位置后删除和插入是比较舒服的

pos后插入

void SLTInsertAfter(SLTNode* pos, SLTDataType x)//pos之后插入
{assert(pos);SLTNode* newnode = BuySLTNode(x);newnode->next = pos->next;pos->next = newnode;
}

这个函数和任意位置插入的区别在于这个不需要pphead,但缺点也是不能头插

pos后删除

void SLTEraseAfter(SLTNode* pos)//pos之后删除
{assert(pos);assert(pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}

与上面的函数相对,还有pos后的删除,也很简单,我们要注意断言一下

以上即为本期的全部内容,希望大家有所收获,最后在这里附上全部代码

全部代码

#pragma once
//SList.h
#include 
#include
#includetypedef int SLTDataType;
typedef struct SListNode {SLTDataType data;struct SListNode* next;
}SLTNode;void SLTPrint(SLTNode* phead);//打印
SLTNode* BuySLTNode(SLTDataType x);//创建新节点
void SLTPushBack(SLTNode** pphead,SLTDataType x);//尾插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//头插
void SLTPopBack(SLTNode** pphead);//尾删
void SLTPopFront(SLTNode** pphead);//头删
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//查找
void SLTInsert(SLTNode** pphead, SLTNode* pos,SLTDataType x);//pos位置前插入
void SLTErase(SLTNode** pphead, SLTNode* pos);//pos位置删除
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//pos之后插入
void SLTEraseAfter(SLTNode* pos);//pos之后删除
#include"SList.h"
//SList.cvoid SLTPrint(SLTNode* phead)//打印
{SLTNode* cur = phead;while (cur) {printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}SLTNode* BuySLTNode(SLTDataType x)//创建新节点
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL) {perror("malloc fail");return NULL;}newnode->data = x;newnode->next = NULL;return newnode;
}void SLTPushBack(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;}
}void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{assert(pphead);SLTNode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}void SLTPopBack(SLTNode** pphead)//尾删
{assert(pphead);//严厉的检查assert(*pphead);//温柔的检查/*if (*pphead == NULL) {return;}*///只有一个节点if ((*pphead)->next == NULL) {free(*pphead);*pphead = NULL;}else {//多个节点/*SLTNode* prev = NULL;SLTNode* tail = *pphead;while (tail->next != NULL) {prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;*/SLTNode* tail = *pphead;while (tail->next->next != NULL) {tail = tail->next;}free(tail->next);tail->next = NULL;}
}
void SLTPopFront(SLTNode** pphead)//头删
{assert(pphead);//严厉的检查assert(*pphead);//温柔的检查/*if (*pphead == NULL) {return;}*/SLTNode* first = *pphead;*pphead = first->next;free(first);first = NULL;
}SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//查找
{SLTNode* cur = phead;while (cur) {if (cur->data == x) {return cur;}cur = cur->next;}return NULL;
}void SLTInsert(SLTNode** pphead, SLTNode* pos,SLTDataType x)//pos位置前插入
{assert(pos);assert(pphead);if (pos == *pphead) {SLTPushFront(pphead, x);}else {//找到pos前的位置SLTNode* prev = *pphead;while (prev->next!=pos) {prev = prev->next;}SLTNode* newnode = BuySLTNode(x);prev->next = newnode;newnode->next = pos;}
}void SLTErase(SLTNode** pphead, SLTNode* pos)//pos位置删除
{assert(pphead);assert(pos);//assert(*pphead);if (pos == *pphead) {SLTPopFront(pphead);}else {SLTNode* prev = *pphead;while (prev->next != pos) {prev = prev->next;}prev->next = pos->next;free(pos);}
}void SLTInsertAfter(SLTNode* pos, SLTDataType x)//pos之后插入
{assert(pos);SLTNode* newnode = BuySLTNode(x);newnode->next = pos->next;pos->next = newnode;
}void SLTEraseAfter(SLTNode* pos)//pos之后删除
{assert(pos);assert(pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}

如有错误,还请指正

相关内容

热门资讯

安卓系统金立手机,品质生活新选... 你有没有发现,最近安卓系统下的金立手机突然火了起来?没错,就是那个曾经陪伴我们走过无数时光的金立手机...
无安卓系统的电视,新型无系统电... 亲爱的读者们,你是否厌倦了那些充斥着安卓系统的电视?想要尝试一些新鲜玩意儿?那就跟我一起探索一下无安...
麒麟系统能刷安卓系统吗,轻松刷... 你有没有想过,你的麒麟手机能不能装上安卓系统呢?这可是个让人好奇不已的问题。现在,就让我来带你一探究...
手机公司安卓系统吗,手机公司引... 你有没有想过,为什么你的手机里装的是安卓系统而不是苹果的iOS呢?这背后可是有着不少故事和门道的哦!...
安卓系统 文件网络传输,安卓系... 你有没有想过,手机里的文件怎么才能轻松地传给朋友呢?今天,就让我来给你揭秘安卓系统中的文件网络传输技...
安卓手机系统怎样备份,安卓手机... 你有没有想过,如果你的安卓手机突然“罢工”了,里面的照片、联系人、应用和数据怎么办?别担心,今天就来...
安卓系统怎样分享app,安卓系... 你是不是也和我一样,手机里装了超多好用的APP,但是有时候想和朋友分享这些宝藏,却不知道怎么操作呢?...
sonicarekids安卓系... 最近是不是你也遇到了Sonicare Kids安卓系统打不开的烦恼?别急,让我来帮你一探究竟,找出解...
安卓刷mac系统教程,体验全新... 你有没有想过,让你的安卓手机也来个华丽变身,摇身一变成为一台Mac电脑呢?别惊讶,这可不是天方夜谭,...
安卓系统根目录删除,深度揭秘删... 你有没有遇到过这种情况:手机里的安卓系统突然出了点小状况,比如不小心点错了某个按钮,结果发现根目录里...
怎么在安卓系统装windows... 你是不是也和我一样,对安卓手机的强大性能爱不释手,但又时不时地想念Windows系统的熟悉界面和那些...
kindle安卓系统壁纸设置,... 亲爱的Kindle用户,你是否曾为你的Kindle设备挑选过一款心仪的壁纸呢?今天,就让我带你一起探...
一加降级安卓系统,回顾与展望 你有没有想过,你的手机系统升级后,竟然还能降级回旧版本?这听起来是不是有点像穿越时空的魔法?没错,今...
凤凰安卓电视系统安装,畅享智能... 亲爱的读者们,你是否也像我一样,对凤凰安卓电视系统安装充满了好奇?想象一台普通的电视,通过安装这个系...
如何更换安卓系统手机,安卓系统... 你有没有想过,你的安卓手机用久了,是不是有点儿卡顿了呢?别急,今天就来教你怎么给它换换“血”,让它焕...
国家粮油统计安卓系统,智能数据... 你有没有想过,每天我们吃的粮食,背后竟然有这么多的故事和数据呢?没错,今天就要带你走进国家粮油统计的...
台电双系统安卓更新,畅享双平台... 你知道吗?最近台电的双系统安卓更新可是引起了不小的轰动呢!作为一个紧跟科技潮流的数码爱好者,我可是迫...
安卓系统上打开caj,Andr... 你有没有遇到过这种情况:手里拿着一本看起来超级有料的电子书,打开一看,哎呀妈呀,竟然是CAJ格式!别...
装安卓手机系统教程,安卓手机系... 你有没有想过,让你的安卓手机也能装上Windows系统,体验一下不一样的操作界面呢?没错,今天就要来...
安卓系统不供应华为,安卓系统不... 你知道吗?最近有个大新闻在科技圈里炸开了锅,那就是安卓系统不再供应华为了!这可不仅仅是两个公司之间的...