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

如有错误,还请指正

相关内容

热门资讯

122.(leaflet篇)l... 听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
Vue使用pdf-lib为文件... 之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vu...
PyQt5数据库开发1 4.1... 文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服...
Android studio ... 解决 Android studio 出现“The emulator process for AVD ...
Linux基础命令大全(上) ♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维...
再谈解决“因为文件包含病毒或潜... 前面出了一篇博文专门来解决“因为文件包含病毒或潜在的垃圾软件”的问题,其中第二种方法有...
南京邮电大学通达学院2023c... 题目展示 一.问题描述 实验题目1 定义一个学生类,其中包括如下内容: (1)私有数据成员 ①年龄 ...
PageObject 六大原则 PageObject六大原则: 1.封装服务的方法 2.不要暴露页面的细节 3.通过r...
【Linux网络编程】01:S... Socket多进程 OVERVIEWSocket多进程1.Server2.Client3.bug&...
数据结构刷题(二十五):122... 1.122. 买卖股票的最佳时机 II思路:贪心。把利润分解为每天为单位的维度,然后收...
浏览器事件循环 事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间࿰...
8个免费图片/照片压缩工具帮您... 继续查看一些最好的图像压缩工具,以提升用户体验和存储空间以及网站使用支持。 无数图像压...
计算机二级Python备考(2... 目录  一、选择题 1.在Python语言中: 2.知识点 二、基本操作题 1. j...
端电压 相电压 线电压 记得刚接触矢量控制的时候,拿到板子,就赶紧去测各种波形,结...
如何使用Python检测和识别... 车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计...
带环链表详解 目录 一、什么是环形链表 二、判断是否为环形链表 2.1 具体题目 2.2 具体思路 2.3 思路的...
【C语言进阶:刨根究底字符串函... 本节重点内容: 深入理解strcpy函数的使用学会strcpy函数的模拟实现⚡strc...
Django web开发(一)... 文章目录前端开发1.快速开发网站2.标签2.1 编码2.2 title2.3 标题2.4 div和s...