单链表-----重置版
创始人
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;
}

如有错误,还请指正

相关内容

热门资讯

哪个安卓机系统好用,探索安卓系... 你有没有想过,手机里的安卓系统就像是个大厨,不同的系统就像不同的烹饪手法,有的让你吃得津津有味,有的...
安卓如何控制苹果系统,从安卓到... 你知道吗?在这个科技飞速发展的时代,安卓和苹果两大操作系统之间的较量从未停歇。虽然它们各自有着忠实的...
安卓原生系统文件夹,安卓原生系... 你有没有发现,每次打开安卓手机,里面那些文件夹就像是一个个神秘的宝箱,里面藏着各种各样的宝贝?今天,...
基于安卓系统的游戏开发,从入门... 你有没有想过,为什么安卓手机上的游戏总是那么吸引人?是不是因为它们就像是你身边的好朋友,随时随地都能...
安卓系统怎样装驱动精灵,安卓系... 你那安卓设备是不是突然间有点儿不给力了?别急,今天就来手把手教你如何给安卓系统装上驱动精灵,让你的设...
如何本地安装安卓系统包,详细步... 你有没有想过,把安卓系统装在你的电脑上,是不是就像给电脑穿上了时尚的新衣?想象你可以在电脑上直接玩手...
安卓12卡刷系统教程,体验全新... 你有没有发现,你的安卓手机最近有点儿不给力了?运行速度慢得像蜗牛,是不是也想给它来个“换血大法”,让...
安卓系统无法打开swf文件,安... 最近是不是发现你的安卓手机有点儿不给力?打开SWF文件时,是不是总是出现“无法打开”的尴尬局面?别急...
鸿蒙系统依赖于安卓系统吗,独立... 你有没有想过,我们手机里的那个鸿蒙系统,它是不是真的完全独立于安卓系统呢?这个问题,估计不少手机控都...
适合安卓系统的图片软件,精选图... 手机里堆满了各种美美的照片,是不是觉得找起来有点头疼呢?别急,今天就来给你安利几款超级适合安卓系统的...
阴阳师安卓系统典藏,探寻阴阳师... 亲爱的阴阳师们,你是否在安卓系统上玩得如痴如醉,对那些精美的典藏式神们垂涎欲滴?今天,就让我带你深入...
安卓系统有碎片化缺点,系统优化... 你知道吗?在手机江湖里,安卓系统可是个响当当的大侠。它那开放、自由的个性,让无数手机厂商和开发者都为...
安卓4系统手机微信,功能解析与... 你有没有发现,现在市面上还有很多安卓4系统的手机在使用呢?尤其是那些喜欢微信的朋友们,这款手机简直就...
鸿蒙系统是安卓的盗版,从安卓“... 你知道吗?最近在科技圈里,关于鸿蒙系统的讨论可是热闹非凡呢!有人说是安卓的盗版,有人则认为这是华为的...
安卓系统怎么剪辑音乐,轻松打造... 你是不是也和我一样,手机里存了超多好听的歌,但是有时候想给它们来个变身,变成一段专属的旋律呢?别急,...
怎么把安卓手机系统变为pc系统... 你有没有想过,把你的安卓手机变成一台PC呢?听起来是不是有点酷炫?想象你可以在手机上玩电脑游戏,或者...
手机怎么装安卓11系统,手机安... 你有没有想过,让你的手机也来个“青春焕发”,升级一下系统呢?没错,就是安卓11系统!这个新系统不仅带...
安卓系统如何拼网络,构建高效连... 你有没有想过,你的安卓手机是怎么和网络“谈恋爱”的呢?没错,就是拼网络!今天,就让我带你一探究竟,看...
安卓系统怎么看小说,轻松畅享电... 你有没有发现,手机里装了那么多应用,最离不开的竟然是那个小小的小说阅读器?没错,就是安卓系统上的小说...
车载安卓系统12v几安,12V... 你有没有想过,你的车载安卓系统升级到12V几安,竟然能带来如此翻天覆地的变化?没错,今天咱们就来聊聊...