数据结构 单链表SingleList【带你从浅入深真正搞懂链表】_singlelist singlemap(4)

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

在这里插入图片描述

对于尾插法,你学【废】会了吗😝


2、尾删【Circuitous】

有尾插,那一定也有尾删,我们一起来探究一下🔍

经典错误案例分析
  • 对于尾删我们一样从一个经典的错误案例开始说起
void SListPopBack(SLTNode\* phead)
{
	SLTNode\* ptail = phead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	free(ptail);
	tail = NULL;
}

  • 对于tail,当其遍历到最后一个结点,也就是next 为空的时候,跳出循环,然后free释放掉这个结点,然后又进行了一步操作将这个tail的值置为空,这个意思就是想着说删除了一个结点后尾结点继续置为空
  • 有不少同学一开始都是这样去是实现的,但这么去写是会出现问题的,通过图示看一下

在这里插入图片描述

  • 通过DeBug我们可以看到,当PopBack函数中将这个ptail置为NULL的时候,确实其内存地址就变成了【0x00000000】,但是这也只是修改了这个局部变量ptail的值,与链表当前一个结点的next域其实没有任何关系,可以看到phead最后一个结点300的next域还是一个地址值,并没有被置空,这其实就相当于一个野指针一般,到后面万一不小心去修改这个next值,就会造成很大的问题,有关野指针的问题,后面我会详细叙述
  • 此时只是这个局部变量做了一个修改,并没有改变这个结构体,要改变结构体就要使用到结构体指针

在这里插入图片描述

修改方式一:保存ptail的上一个结点
  • 首先来看第一种修改方式,也就是将这个ptail向后遍历的时候先做一个保存,放到一个结构体指针prev中,在向后遍历的时候若ptail遍历到最后一个结点,直接free释放ptail即可,然后让prev->next = NULL,这个时候结构体就发生了改变,这个next指针域也发生了真正的改变,不会像上面一样变成一个野指针充满【❗dangerous❗】
//way1
SLTNode\* ptail = phead;
SLTNode\* prev = NULL;
while (ptail->next != NULL)
{
	prev = ptail;
	ptail = ptail->next;
}
free(ptail);
prev->next = NULL;

在这里插入图片描述

修改方式二:ptail->next->next
  • 然后说说第二种修改方式,也就是不需要去再定义一个prev指针去保存,而是直接判断【ptail->next->next】是否为空即可,此时这里的ptail便为尾结点的前一个结点,因此free(ptail->next)就是删除尾结点,最后将这个结点置为空
//way2
SLTNode\* ptail = phead;
while (ptail->next->next != NULL)
{
	ptail = ptail->next;
}
free(ptail->next);
ptail->next = NULL;

在这里插入图片描述

特殊情况修正【单个结点、二级指针修改、断言报错】
  • 你以为用这样就可以真正地实现尾删了吗,那还远远不够,路还有很远🐎,继续分析下去吧
  • 可以看到,在这里我执行了三次PopBack,然后到最后一次的时候,发现ptail->next==NULL,但是ptail->next->next却是一个越界的位置,这个时候其实就不对了,之前我们有说过,访问越界是一个很大的问题🈲

在这里插入图片描述

  • 所以,对于单个结点的尾删,我们应该进行一个单独的判断。也就是当前传入进来然后接收的这个phead所指向的next是否为空。若为空,则表示此单链表只有一个结点,直接free这个phead即可,然后将其置为NULL
void SListPopBack(SLTNode\* phead)
{
	if (phead->next == NULL)
	{		//只有一个结点
		free(phead);
		phead = NULL;
	}
	else
	{
		SLTNode\* ptail = phead;
		while (ptail->next->next != NULL)
		{
			ptail = ptail->next;
		}
		free(ptail->next);
		ptail->next = NULL;
	}
}


  • 那有同学说,这个时候应该没问题了吧,单个结点的情况都被我考虑到了,简直天衣无缝😏
  • 但是。。。Exception它又来了

在这里插入图片描述

  • 不说了,没有爱了❤️直接DeBug吧⌨️
  • 那这里是又可以看到,我们在写尾插的时候出现过的情况,就是函数内部做了改变但是外界并不知晓,那这个时候该怎么办呢?没错,就是它,👉二级指针👈

在这里插入图片描述

  • 那这下可好了,整个函数的结构又要重新定义,这其实是正常的,调Bug💻就是不断在试错的过程,当你找出错来了,那就应该及时更正,不要怕麻烦
  • 一样,就像下面这么改,对于只有单个结点需要单独处理时使用*pphead去获取一级指针进行操作~
void SListPopBack(SLTNode\*\* phead)
{
	if ((\*phead)->next == NULL)
	{		//若只有一个结点,直接将其释放,传入二级指针便改变了链表的头结点
		free(\*phead);
		\*phead = NULL;
	}
	else
	{
		SLTNode\* ptail = \*phead;
		while (ptail->next->next != NULL)
		{
			ptail = ptail->next;
		}
		free(ptail->next);
		ptail->next = NULL;
	}
}

  • 然后我们再来看一下运行结果,可以看到,链表终于被清干净了💧

在这里插入图片描述

  • 真的是天衣无缝、百无一失了吗?我再删一次你看看😱

在这里插入图片描述

  • 可以看到,此时的链表已经为空,但是我又执行了一次PopBack操作,那有同学说,那你这不是手欠吗,人家都被删空了你还要再删一次,成心跟人过不去是吧😠
  • 记住一点,你永远要考虑要一些随性操作的用户,指不定那天进行了一些你开发时想都想不到的操作😈

在这里插入图片描述

  • 这其实就是什么问题?没错,就是访问越界的问题,当你将链表已经删空的时候,再去对链表进行一个删除,此时访问的就是一个随机的位置,超出了你所能访问的界限,编译器也就很好地为你【检查出了错误】
  • 那此时我们应该怎么办呢?没错,就是在PopBack函数进来的一开始,就进行一个判断,看看这个指针所指向的头是否为空,若是为空,则执行相应的报错
  • 在这里我们选择直接使用暴力的方式,也就是断言【assert】,不要温柔的if判断,直接报出来哪里有问题,然后打一顿再说💪
void SListPopBack(SLTNode\*\* phead)
{
	assert(\*phead);
	if ((\*phead)->next == NULL)
	{		//若只有一个结点,直接将其释放,传入二级指针便改变了链表的头结点
		free(\*phead);
		\*phead = NULL;
	}
	else
	{
		SLTNode\* ptail = \*phead;
		while (ptail->next->next != NULL)
		{
			ptail = ptail->next;
		}
		free(ptail->next);
		ptail->next = NULL;
	}
}

以上就是尾删法的最终版本,你又学【废】了吗😝


说完尾插和尾删,还有头插和头删,但是不要惊慌,对于头插和头删,没有你想象得那么复杂😵

3、头插【Easy】

  • 对于头插其实并不复杂,也就是创建出一个新的结点newnode,然后让这个结点的next域存放首个结点的地址,还是一样,对于头插和头删,都是要使用到二级指针的,否则内部的改动是无法带动外部的变化
  • 要将这个二级指针化为一级指针我们说过很多遍,一次*解引用即可。链接上后将这个新的头所存在的地址给到指向头结点的指针即可

在这里插入图片描述

void SListPushFront(SLTNode\*\* pphead, SLTDataType x)
{
	SLTNode\* newnode = BuySLTNode(x);
	newnode->next = (\*pphead);		//二级指针解引,变为一级指针
	\*pphead = newnode;		//再让newnode变为新的头
}

  • 怎么样,简单吧,很快就说完了,一方面是因为我们前面铺垫得很多,你所掌握的东西我已经不需要说了,另一方面在对于单链表而言,它的头插和头删确实比尾插和尾删来得高效

4、头删【Easy】

  • 说完头插,马上趁热打铁来说说头删
  • 还是一样,并不复杂。因为删除这个头,那当其删除之后,它的后一个结点也就成为了新的头,这个时候就需要做一个更新,但这个前提是你能访问到后面这个结点,所以我们要事先去保存当前头结点的后一个结点,然后将头结点free释放,最后更新让头结点指针重新指向下一个新的结点即可

在这里插入图片描述
在这里插入图片描述

void SListPopFront(SLTNode\*\* pphead)
{
	assert(\*pphead);	//删除都先断言一下,看看传进来的链表是否为空

	SLTNode\* nextNode = (\*pphead)->next;	//先保存当前首结点的下一个结点
	free(\*pphead);
	(\*pphead) = nextNode;
}

5、查找

  • 看完了尾插、尾删、头插、头删。接下来我们要学习的是去链表中查找指定元素,先来看看接口定义
SLTNode\* SListFind(SLTNode\* phead, SLTDataType x);

  • 可以看到,形参接收的是头结点指针,以及需要查找的值,返回值类型是一个结构体指针,也就是说返回的是一个指向待查结点的结构体指针,并不是一个下标,一个位置,那函数内部我们该如何去写呢?
  • 很简单,只需要一个cur指针先获取头结点的指向,然后一个个向后查询即可,若是发现其指向的data值相同,便返回当前的cur指针
SLTNode\* cur = phead;
//while (cur != NULL)
while (cur)
{
	if (cur->data == x)
		return cur;
	cur = cur->next;
}
return NULL;
//返回的是指向这个待查结点的指针,并不是位置

  • 我们来测试一下看看
SLTNode\* SList = NULL;
SListPushBack(&SList, 1);
SListPushBack(&SList, 2);
SListPushBack(&SList, 3);
SListPushBack(&SList, 4);
SListPushBack(&SList, 5);
PrintList(SList);

SLTNode\* pos = SListFind(SList, 3);
if (pos != NULL) {
	printf("找到了\n");
	printf("pos = %d\n", pos->data);
}
else {
	printf("没找到\n");
}

在这里插入图片描述

  • 可以看到,确实是可以找到,我将这个结点的data值打印了出来

那有同学问,找到返回了这个值又能怎样呢,可以拿来做什么?那我告诉你这个用处可大了,后面我们将在找到的这个pos所指的结点之前、之后进行插入和删除相应的结点,都是要以这个Find接口作为前提

6、在pos位置之后插入结点

  • 接下来我们就来看一看如何在找到的pos位置之后插入一个结点呢

在这里插入图片描述

  • 可以看到,我们最后要达到的是这种效果,就是pos的next要指向这个newnode,然后newnode的next要指向pos->next,这时候有些同学就会这么去写。先让这个pos的next指向newnode,然后再让newnode的next指向pos的next
  • 这个逻辑其实就有问题,当pos的next所指位置变化之后也就找不到4这个结点了,执行完第一条语句后pos的next就变成了newnode,这个时候再让newnode指向pos的next,也就是让newnode指向它自己,这也就不对了
//pos的下一个位置就没了,相当于是newnode自己指向自己
pos->next = newnode;
newnode->next = pos->next;

  • 所以应该把这两条语句做一个交换。先让newode的next指向pos的next,也就是4,然后再让pos指向这个newnode,这个时候结点之间的链接就没问题了😯
newnode->next = pos->next;
pos->next = newnode;

  • 通过这段代码,我们插入一个值试试看。从打印结果看来,确实在3后面插入了一个9

在这里插入图片描述

警惕传入空指针【✒细致讲解断言assert】
  • 还是一样,继续思考有没有可能出现特殊情况,我们来看看这种
  • 可以看到,链表中的结点值只有1~5但是却需要找一个88,很明显是找不到的,此时便会return NULL,那么外界的pos接受到的值就是一个空值,也就说这是一个空指针,所以当你传一个空指针进去,你让函数内部怎么实现一个插入呢
SLTNode\* pos = SListFind(SList, 88);
SListInsertAfter(pos, 9);
PrintList(SList);

  • 此时可以看到,程序发生了奔溃/(ㄒoㄒ)/~~

在这里插入图片描述

  • 看到这个地方,我相信你的第一反应就是在函数一开始的加一个断言,判断这个pos是否为空
  • 可能有些同学不了解里面的传参机制以及判断机制,我这里讲一下
  • 对于assert断言来说,就是还函数还没有执行的时候去做的一件事,所以要放到一段程序的开头,而不是应该把它放在函数体的中间或者结尾,这点首先要明确;
  • 其次assert断言的内部参数你需要传入的是你需要检查的传入进来的某个参数变量,一般是去判断是否为空或者是否小于0。所以对于参数,你应该写入的是会报出错误的对立面,举个例子,当你想要检查a是否大于0,a小于0就会报错,因此应该写assert(a > 0)。若是去判断一个指针是否为空,就要这样写:assert(p != NULL),或者直接简写成assert( p ),就像我们的while循环去判空是一个道理;
  • 知道了传参逻辑,最后说说assert的判断机制,就是当你所写的表达式不成立时,也就是当【p == NULL】时,就会出现警告,报出错误。在计算机中【0是假,非0才是真】,当【p = NULL】时,这个表达式就变为了假,非0一般指1,就是p != NULL,表明传进来的指针p不为空,那也就是不会满足条件
  • 我们通过CPlusPlus来看看,明确说到当表达式的值为0的时候,就会向标准的错误设备写入一条消息终止调用,也就是断言assert后面的语句均不会执行

在这里插入图片描述

  • 所以应该在InsertAfter函数的开头加上这一句话
//0是假,非0才是真
assert(pos != NULL);	//若不为空,则不会执行;若为空,则报出警告
//assert(pos);

  • 此时可以看到,加上这句话后断言就出现了,直接给你报出了哪个源文件的哪一行出了问题,也就是我们刚才写断言的地方,这个时候你立马可以进行【精确定位】然后排错,这个时候也就提高了程序的可维护性,所以作为我们程序员来说,学会使用断言是很重要的,但是断言并不是什么地方都可以随便加的,根据需求来加,也就是【因地制宜】,不然会出现麻烦,结尾总结的时候会说到📖

在这里插入图片描述

【生活小案例3——请人吃饭要带钱💴】
  • 通过传入空指针去插入一个结点可以看到是一个很荒谬的事情,我们联想一个生活中的小案例来理解一下,顺便轻松一刻🎸
  • 你传入一个空指针和一个待插入的结点值,其实就相当于你带你说今天请你朋友去外面吃饭,但是吃饭之后一看钱包是空的,于是这个时候就只能让你朋友付钱了【假设不支持手机支付】,那你说这个事情是不是很荒谬,assert断言其实就是在出门之前检查一下你有没有带钱,如果没有带那就会报出警告提醒你要带钱出门
  • 所以在函数进行传递指针的时候一定要检查传递的是不是空指针,就想出门检查自己有没有带钱是一个道理

7、在pos位置之前插入结点

  • 说完了在这个pos位置之后插入结点,那有后一定有前,现在我们来谈一谈如何在获取到的pos位置前插入一个结点。对于这个,说在前面,会出现两种情况:
    • 一种是当前找到的pos在链表中间的某个地方
    • 还有一种比较特殊,就是这个pos刚好是头结点,这个时候进行的其实就是我们上面说过的头插法,那既然是头插法,就需要去改变这个链表的头,所以又需要使用到二级指针
void SListInsertBefore(SLTNode\*\* pphead, SLTNode\* pos, SLTDataType x)
{
	if (\*pphead == pos) {
		//若pos位置就为原先的头结点,则使用头插法进行插入
		SListPushFront(pphead,x);
	}
	else {
		SLTNode\* cur = \*pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		SLTNode\* newnode = BuySLTNode(x);
		//此处无需考虑前后错乱问题,直接链接即可
		cur->next = newnode;
		newnode->next = pos;
	}
}

  • 直接给出代码做讲解。一样的老套路,通过*pphead进行解引用,获取到外界SList这个头结点指针,然后去进行一个判断即可,若这个 *phead和pos相同,那表明所Find找到的pos就为链表的头结点。直接调用一下我们上面写过的头插法即可,实现了功能的复用,具体图示如下👇
    在这里插入图片描述
  • 然后再来说一下普通情况,也就是在链表其中的某一个位置前插入,所以我们要获取pos指针所指向的前一个位置,开头说到过,虽然单链表对于插入和删除很方便,但是访问结点并不方便,需要从头结点开始访问,于是还是一样的套路,定义一个cur指针首先获取到头结点指针的值,也就是把二级指针pphead解引用一下,然后一直去遍历即可,直到这个【cur->next = pos】为止停下来,表示当前cur所指向的结点所在的下一个位置便是pos,此时就可以做一个链接了,具体图示如下👇
  • 可以看到,此时无需像在后面插入结点一般去考虑这个先后的顺序关系,只需要将【cur】【newnode】【pos】这三个指针做一个链接即可

在这里插入图片描述

  • 最后我们来通过测试验证一下结果
  • 这是普通情况

在这里插入图片描述

  • 来看特殊情况

在这里插入图片描述

看完了如何插入结点,接下去我们来说说如何在查找到的pos位置前后删除结点

8、删除pos位置之后的结点

  • 首先来说一说如何删除pos位置之后的结点
  • 首先来看一下函数体声明,很明显,只需要传入一个pos指针即可,其余不需要
void SListEraseAfter(SLTNode\* pos)

  • 然后我们来考虑函数体内部,首先要做的事情相信你已经很敏感了,对于删除我们都要assert一下这个传入的指针是否为空,若为空则不能对其进行操作。然后需要考虑的就是特殊情况,有什么特殊情况呢?就是下面这种pos为最后一个尾结点时,后面没有结点可删,此时直直接return返回即可,当然你也可以用assert断言

在这里插入图片描述

  • 然后就是这种正常的情况,我们需要修改pos所在结点的指针域,因为删除了后一个结点,所以要将其指针域改为下一个结点的再下一个结点,于是有同学就会这么写。这里写其实是有问题的,从内存的角度来说,因为pos的next已经改变了,此时【pos->next】这块地址已经访问不到了,再去free的话也是徒劳
pos->next = pos->next->next;
free(pos->next);

  • 因为我们需要将【pos->next】做一个临时保存,这样当pos的next指针域改变时,也可以访问到这个被删除的结点

在这里插入图片描述

  • 具体代码如下
void SListEraseAfter(SLTNode\* pos)
{
	assert(pos);
	SLTNode\* nextNode = pos->next;
	if (nextNode == NULL){
		return;		//考虑到所查找到的结点为最后一个结点,后面无结点可删
	}
	else {
		pos->next = nextNode->next;
		free(nextNode);
	}
}

  • 那这个时候有同学又来抬杠说,再定义一个看着冗杂,为啥不先把这个结点释放,然后再修改指针域呢?
  • 大家觉得这样可以吗❓
free(pos->next);
pos->next = pos->next->next;

  • 其实这是一种非常典型的错误,很多同学都容易犯,当你将这个结点free掉之后,那也就是说其data域和next都没了,但是待删结点3的next域存放有结点4的地址,你讲它提前释放了,那pos怎么找得到呢❓此时【pos->next】就变成了一种我们都听说过的东西,叫做【野指针】,可能有小伙伴没听说过,也没关系,我们可以来了解一下👇
野指针的危害【生活小案例4——酒店房门的钥匙🔑】
  • 就我们理解而言,指针都是指向一个地址,野指针也不例外,它指向一个地址,但是呢,这块地址不是确定的,而是随机的
  • 这一块我不是非常了解,大家可以看看这篇博客——》野指针的产生及其危害

通过阅读这篇文章,我也了解到了一些有关野指针的知识,但是这么理解起来比较晦涩难懂,因为还是按照我们的惯例,通过生活的一个小案例来帮助大家理解

  • 首先来说一说释放结点的含义:对于结点的释放呢,并不是把此结点存放的地址给销毁了,而是此结点所存放的地址的使用权不属于你了,还给了操作系统。对于申请内存就像去酒店开房间,这个房间的使用权就属于你了,不会有人突然闯进来,那释放了就相当于是退房了,这个房间的使用权就不属于你了,但是这个房间还在【这一点很重要,这个房间还在!!】
  • 刚才说到酒店开房,我们具体来说说:你在酒店🏨开了一个房间,住了一晚后把这个房间退了,但是在走之前打电话找了一个锁匠根据这个房间的所配了一把钥匙。野指针访问随机内存就好比你后一个晚上没有登记入住但是却通过这把自己装配的钥匙进入了这个房间,又在里面住了一个晚上然后这个晚上刚好没什么客人,保洁阿姨也请假了,所以你又白嫖了一个晚上😊。于是你打算明天再来住一次,可是这一次,却有很大的祸患,晚上来了一个旅行团,于是很多房间都要重新打扫,然后就发现你还住在这里,就报警把你抓了起来👮
  • 这把钥匙其实就相当于是野指针。野指针真正的危害在于【这块内存已经释放了,是一个随机值,但还有一个指针指向这块地址,但是这个地址是不被保护的,随时都有可能出问题】
  • 所以对于野指针的访问不一定会报错,取决你有没有被编译器查到💻

通过这么一个案例,大家对野指针这一块应该有所了解了,所以不要轻易将指针乱指,可能那块地址就是随机的


  • 看了野指针的危害,我们继续回到代码中来DeBug看看,使用野指针会出现什么情况
free(pos->next);
pos->next = pos->next->next;

  • 从图中可以看出,一开始进去的时候,pos->next也就是我们定义的nextNode,存有4的地址,但是当我们先去free之后,这个地址就不见了,而且数据值也变成了一个随机值,所以这个时候再去访问pos->next就是一个【野指针】了

在这里插入图片描述
在这里插入图片描述

  • 但是这个时候编译器却不会报错,正常运行下去了,此时我们来看看外界的SList
  • 可以看出,确实出问题了,我们需要删除的是pos所指向的next,也就是3这个位置,但是当先free之后,此时4也没有了,找不到了,2的next指向的是一个随机的地址,那编译器对于这种问题不会报错吗?当然会,但不会是在这里,而是在Print中

在这里插入图片描述

  • 因为打印的时候需要随着next指针所存放的地址一一访问,因此当访问到2的next时,便出现了访问异常,因为它访问的是一块不确实的地址,是没有被分配的,里面没有任何东西,因此这时编译器就会报出异常

在这里插入图片描述

所以大家在进行指针操作的时候一定要小心,可以一疏忽你的指针就变成了野指针🗡

9、删除pos位置之前的结点【比较综合✔】

  • 说完了pos位置之后的结点删除操作,那之前的一定也要说,和这个和插入一样,会有多种情况,可能会改变头结点,因此需要使用到二级指针
void SListEraseBefore(SLTNode\*\* pphead, SLTNode\* pos)

第一种:pos就位于头结点位置

  • 这种情况的话是无法删除的,因为头结点前面为空,所以直接返回即可,当然你也可以assert
//当pos就为头结点时
if ((\*pphead) == pos) {
	return;
}

第二种:pos位于头结点的后一个位置,需要删除的便为头结点

  • 这个的话就是我们前面讲到过的头删,直接调用即可

在这里插入图片描述

else if ((\*pphead)->next == pos) {
	SListPopFront(pphead);		//头删
}

第三种:正常情况,但是比较繁琐

  • 对于正常情况,其实是比较麻烦的,我们来看看

在这里插入图片描述

  • 对于这种情况,因为我们是要去删除pos结点的前一个结点,但是我们知道,要删除一个结点,就要找到它的前一个结点,因此这个时候我们需要定义一个结构体指针cur,刚开始指向【*pphead】,在不断往后遍历的时候当【cur->next->next == pos】时,定义一个指针指向当前cur的next,进行一个保存,接着执行【cur->next = delnode->next】,便可以进行一个删除
  • 具体代码如下
void SListEraseBefore(SLTNode\*\* pphead, SLTNode\* pos)
{
	assert(pos);		//删除结点要断言
	assert(\*pphead);		

	//当pos就为头结点时
	if ((\*pphead) == pos) {
		return;
	}			//当删除的结点为头结点时
	else if ((\*pphead)->next == pos) {
		SListPopFront(pphead);		//头删
	}
	else {
		SLTNode\* cur = \*pphead;
		while (cur->next->next != pos)
			cur = cur->next;
		SLTNode\* delnode = cur->next;
		cur->next = delnode->next;
		free(delnode);
	}
}

  • 我们来测试一下看看

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

10、释放链表

  • 最后的压轴💃当然是给到Destroy,既然Create了,那一定要Destroy,有始有终,才是最好
  • 首先说一下整体思路。对于单链表来说,它和顺序表并不一样,因为对于顺序表而言,是一块连续的存储空间,申请的时候是一整块一起申请,释放的时候自然也是一整块一起释放,因此直接free即可。但是对于链表不同,链表的每一个结点在堆内存中的空间是随机申请的。因此存储是不连续的,那对于释放来说,就需要从头结点开始,一一释放下去
  • 首先还是一样,保留好头结点,拿一个指针代替,接着在释放头结点的时候还要先行保存其下一个结点,然后free掉当前的cur结点,让cur结点指向下一个结点,继续进行一个释放

在这里插入图片描述

  • 看一下代码
void SLIstDestroy(SLTNode\*\* pphead)
{
	SLTNode\* cur = \*pphead;

	while (cur)
	{
		SLTNode\* nextNode = cur->next;
		free(cur);

		cur = nextNode;
	}
}

  • 你觉得这样可以了吗?那有同学可以说我这么问肯定是不可以,那你知道缺了什么吗,我们将一个单链表Destory一下试试,然后将其打印出来再看看

在这里插入图片描述

  • 是的,可以看到又是我们所熟悉的野指针,为什么呢?因为你将链表的头结点释放了,那这就是一块随机的地址,上面说到过,访问一块随机的地址,也就形成了【野指针】
【生活小案例5——利剑不锋利🗡】
  • 上面又说到了这个野指针的问题,我们再来谈一谈,对于野指针,也是它就像是一把锋利的剑一样,非常危险,但是呢,它又不是随时都会有危险,因为当这把剑放在剑鞘中时,其实是非常安全的,并不是伤害到你,但是当你将它把出来的时候,那这个时候你就要小心使用了,一不留神可能就会伤到自己。
  • 对于野指针也是一样,有的时候你知道这个指针可能会是野指针,但是你不去使用它访问数据,那其实是很安全的,不会有问题,但是当你使用到了这个野指针去访问的时候,其实就会非常危险了。所以在这里还是和大家说一句:谨慎使用指针

好,我们回归正题,既然这样会造成野指针,那应该怎么修改呢,那就是将和这个头结点指针置为NULL即可,这个时候再去打印的时候这个链表就是空的,也就不会出错了

在这里插入图片描述


四、整体代码展示【需要自取】

SList.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLTDataType;
typedef struct SListNode {	//结构体大小:8B
	SLTDataType data;
	struct SListNode\* next;
	//SLT\* next; 不可以这样定义,因为还没有到声明
}SLTNode;

SLTNode\* BuySLTNode(SLTDataType x);
SLTNode\* SListCreate(int n);
void PrintList(SLTNode\* phead);

void SListPushBack(SLTNode\*\* phead, SLTDataType x);
void SListPopBack(SLTNode\*\* phead);
void SListPushFront(SLTNode\*\* pphead, SLTDataType x);
void SListPopFront(SLTNode\*\* pphead);

SLTNode\* SListFind(SLTNode\* phead, SLTDataType x);
void SListInsertAfter(SLTNode\* pos, SLTDataType x);
void SListInsertBefore(SLTNode\*\* pphead, SLTNode\* pos, SLTDataType x);
void SListEraseAfter(SLTNode\* pos);
void SListEraseBefore(SLTNode\*\* pphead, SLTNode\* pos);
void SLIstDestroy(SLTNode\*\* pphead);

SList.cpp

#define \_CRT\_SECURE\_NO\_WARNINGS 1
#include "SList.h"
//-----------------
/\*动态开辟结点\*/
SLTNode\* BuySLTNode(SLTDataType x)
{
	//动态开辟
	SLTNode\* newnode = (SLTNode\*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
}
//-----------------
/\*建立链表\*/
SLTNode\* SListCreate(int n)
{
	SLTNode\* phead = NULL, \* ptail = NULL;
	for (int i = 0; i < n; ++i)
	{
		SLTNode\* newnode = BuySLTNode(i);
		if (phead == NULL)
			phead = ptail = newnode;
		else
		{
			ptail->next = newnode;
			ptail = newnode;
		}
	}
	return phead;
}
//-----------------
/\*打印链表\*/
void PrintList(SLTNode\* phead)
{
	//无需断言,链表为空,可以打印
	SLTNode\* cur = phead;	//尽量不用头指针,定义变量存放【4 byte】
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		//printf("[%d|%p]->", cur->data,cur->next);
		cur = cur->next;
	}
	printf("NULL\n");
}
//-----------------
/\*尾插\*/
void SListPushBack(SLTNode\*\* pphead, SLTDataType x)
{
	//无需断言,链表为空,可以尾插
	SLTNode\* newnode = BuySLTNode(x);
	if (\*pphead == NULL)
	{
		\*pphead = newnode;
		//通过二级指针的解引用改变头结点【从NULL->newnode】
	}
	else
	{
		SLTNode\* pptail = \*pphead;
		while (pptail->next)
		{
			pptail = pptail->next;
			//无需再使用二级指针,无需改变头结点,只需要做一个链接
			//改变结构体的指向,使用结构体指针
		}
		pptail->next = newnode;
	}
}
//-----------------
/\*尾删\*/
void SListPopBack(SLTNode\*\* phead)
{
	assert(\*phead);		//必须断言,链表为空,不可尾删
	if ((\*phead)->next == NULL)
	{		//若只有一个结点,直接将其释放,传入二级指针便改变了链表的头结点
		free(\*phead);
		\*phead = NULL;
	}
	else
	{
		SLTNode\* ptail = \*phead;
		while (ptail->next->next != NULL)
		{
			ptail = ptail->next;
		}
		free(ptail->next);
		ptail->next = NULL;
	}
}
//-----------------
//头插
void SListPushFront(SLTNode\*\* pphead, SLTDataType x)
{
	//无需断言,链表为空,可以头插
	SLTNode\* newnode = BuySLTNode(x);
	newnode->next = (\*pphead);		//二级指针解引,变为一级指针
	\*pphead = newnode;		//再让newnode变为新的头
}

//-----------------
//头删
void SListPopFront(SLTNode\*\* pphead)
{
//必须断言,链表为空,不可头删
	assert(\*pphead);	//删除都先断言一下,看看传进来的链表是否为空

	SLTNode\* next = (\*pphead)->next;	//先保存当前首结点的下一个结点
	free(\*pphead);
	(\*pphead) = next;
}
//-----------------
//查找
SLTNode\* SListFind(SLTNode\* phead, SLTDataType x)
{
	SLTNode\* cur = phead;
	//while (cur != NULL)
	while (cur)
	{
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	return NULL;
	//返回的是指向这个待查结点的指针,并不是位置
}
//-----------------
//在pos位置之后插入结点
void SListInsertAfter(SLTNode\* pos, SLTDataType x)
{
	//0是假,非0才是真
	assert(pos != NULL);	//若不为空,则不会执行;若为空,则报出警告
	SLTNode\* newnode = BuySLTNode(x);
	//pos->next = newnode;
	//newnode->next = pos->next;
	//pos的下一个位置就没了,相当于是newnode自己指向自己

	newnode->next = pos->next;
	pos->next = newnode;
}
//-----------------
//在pos位置之前插入结点
void SListInsertBefore(SLTNode\*\* pphead, SLTNode\* pos, SLTDataType x)
{
	if (\*pphead == pos) {
		//若pos位置就为原先的头结点,则使用头插法进行插入
		SListPushFront(pphead,x);
	}
	else {
		SLTNode\* cur = \*pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		SLTNode\* newnode = BuySLTNode(x);
		//此处无需考虑前后错乱问题,直接链接即可
		cur->next = newnode;
		newnode->next = pos;
	}
}
//-----------------
//删除pos位置之后的结点
void SListEraseAfter(SLTNode\* pos)
{
	assert(pos);
	SLTNode\* nextNode = pos->next;
	if (nextNode == NULL){
		return;		//考虑到所查找到的结点为最后一个结点,后面无结点可删
	}
	else {
		//free(pos->next); //会产生野指针
		//pos->next = pos->next->next;

		pos->next = nextNode->next;
		free(nextNode);
	}
//-----------------
//删除pos位置之前的结点
void SListEraseBefore(SLTNode\*\* pphead, SLTNode\* pos)
{
	assert(pos);		//删除结点要断言
	assert(\*pphead);		

	//当pos就为头结点时
	if ((\*pphead) == pos) {
		return;
	}			//当删除的结点为头结点时
	else if ((\*pphead)->next == pos) {
		SListPopFront(pphead);		//头删
	}
	else {
		SLTNode\* cur = \*pphead;
		while (cur->next->next != pos)
			cur = cur->next;
		SLTNode\* delnode = cur->next;
		cur->next = delnode->next;
		free(delnode);
	}
}
//-----------------
//释放链表
void SLIstDestroy(SLTNode\*\* pphead)
{
	SLTNode\* cur = \*pphead;

	while (cur)
	{
		SLTNode\* nextNode = cur->next;
		free(cur);

		cur = nextNode;
	}
	\*pphead = NULL;
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <malloc.h>
#include "SList.h"

/\* 顺序表缺陷
\* 1.空间不够,需要扩容。扩容(尤其是异地扩容)是有一定代价的。其次还可能存在一定的空间浪费
\* --》扩容都是扩2倍,扩得多了,一些不用空间的就会浪费
\* --》扩得少了,插入一些数空间又不够了,又需要频繁的扩容
\* 
\* 【吃米饭】一碗不够,吃两碗
\* 吃一碗零20粒,频繁去锅里舀饭
\* 
\* 2.头部或者中部插入删除,需要挪动数据,效率低下
\*/

/\* 优化方案
\* 1.按需申请释放【要存储一个数据就开一块空间(结点)】
\* 2.不要挪动数据【指针可以存放下一块空间的地址】
\*/

/\*
\* 顺序表支持随机访问,可以根据下标快速访问到某个元素
\* 链表不支持随机访问,只能通过头结点的next指针域一个个访问下去,最坏会到达O(N)
\* 假设顺序表是货车,链表是公交车,都是不可替代的
\*/
void TestSList1()
{	
	SLTNode\* n1 = (SLTNode\*)malloc(sizeof(SLTNode));
	SLTNode\* n2 = (SLTNode\*)malloc(sizeof(SLTNode));
	n1->next = n2;

	SListNode n3, n4;
	n3.next = &n4;

}

void TestSList2()
{
	/\*\*/SLTNode\* n1 = BuySLTNode(1);
	SLTNode\* n2 = BuySLTNode(2);
	SLTNode\* n3 = BuySLTNode(3);
	SLTNode\* n4 = BuySLTNode(4);
	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = NULL;

	SLTNode\* SList = SListCreate(5);
	PrintList(SList);
}

void Swap1(int\* p1, int\* p2)
{
	int tmp = \*p1;
	\*p1 = \*p2;
	\*p2 = tmp;
}

void Swap2(int\*\* pp1, int\*\* pp2)
{
	int\* tmp = \*pp1;
	\*pp1 = \*pp2;
	\*pp2 = tmp;
}
void test1()
{
	int a = 2, b = 5;
	//printf("a = %d, b = %d\n", a, b);
	//Swap1(&a, &b);
	//printf("a = %d, b = %d\n", a, b);

	int\* p1 = &a, \* p2 = &b;
	printf("\*p1 = %d, \*p2 = %d\n", \*p1, \*p2);
	Swap2(&p1, &p2);
	printf("\*p1 = %d, \*p2 = %d\n", \*p1, \*p2);

	printf("a = %d, b = %d\n", a, b);
}

//尾插
void TestSList3()
{
	//SLTNode\* SList = SListCreate(10);

	//PrintList(SList);

	//SListPushBack(SList, 100);
	//SListPushBack(SList, 200);
	//SListPushBack(SList, 300);

	SLTNode\* SList = NULL;
	SListPushBack(&SList, 100);
	SListPushBack(&SList, 200);
	SListPushBack(&SList, 300);
	PrintList(SList);
}

//尾删
void TestSList4()
{
	SLTNode\* SList = NULL;
	SListPushBack(&SList, 100);
	SListPushBack(&SList, 200);
	SListPushBack(&SList, 300);
	PrintList(SList);

	SListPopBack(&SList);
	PrintList(SList);

	SListPopBack(&SList);
	PrintList(SList);

	SListPopBack(&SList);
	PrintList(SList);

	//再多删一次
	SListPopBack(&SList);	//访问越界
	PrintList(SList);
}

//头插 — 头删
//void TestSList5()
//{
// SLTNode\* SList = NULL;
// printf("尾插:");
// SListPushBack(&SList, 100);
// SListPushBack(&SList, 200);
// SListPushBack(&SList, 300);
// PrintList(SList);
//
// SLTNode\* SList2 = NULL;
// printf("头插:");
// SListPushFront(&SList2, 100);
// SListPushFront(&SList2, 200);
// SListPushFront(&SList2, 300);
// SListPushFront(&SList2, 400);
// PrintList(SList2);
//
// printf("头删:\n");
// SListPopFront(&SList2);
// PrintList(SList2);
// SListPopFront(&SList2);
// PrintList(SList2);
// SListPopFront(&SList2);
// PrintList(SList2);
// SListPopFront(&SList2);
// PrintList(SList2);
//}

//查找与插入
void TestSList6()
{
	SLTNode\* SList = NULL;
	SListPushBack(&SList, 1);
	SListPushBack(&SList, 2);
	SListPushBack(&SList, 3);
	SListPushBack(&SList, 4);
	SListPushBack(&SList, 5);
	PrintList(SList);

	//SLTNode\* pos = SListFind(SList, 3);

	//if (pos != NULL) {
	// printf("找到了\n");
	// printf("pos = %d\n", pos->data);
	// SListInsertAfter(pos, 9);
	// PrintList(SList);
	//}
	//else {
	// printf("没找到\n");
	//}

	//SLTNode\* pos = SListFind(SList, 88);
	//SListInsertAfter(pos, 9);
	//PrintList(SList);

	//SLTNode\* pos = SListFind(SList, 3);
	//SListInsertAfter(pos, 9);
	//PrintList(SList);

	//SListInsertBefore(&SList, pos, 11);
	//PrintList(SList);

	SLTNode\* pos = SListFind(SList, 1);
	SListInsertBefore(&SList, pos, 8);
	PrintList(SList);
}
//删除之后的结点
void TestSList7()
{
	SLTNode\* SList = NULL;
	SListPushBack(&SList, 1);
	SListPushBack(&SList, 2);
	SListPushBack(&SList, 3);
	SListPushBack(&SList, 4);
	PrintList(SList);

	SLTNode\* pos = SListFind(SList, 2);
	SListEraseAfter(pos);
	PrintList(SList);
}
void TestSList8()
{
	SLTNode\* SList = NULL;
	SListPushBack(&SList, 1);
	SListPushBack(&SList, 2);
	SListPushBack(&SList, 3);
	SListPushBack(&SList, 4);
	PrintList(SList);

	//SLTNode\* pos = SListFind(SList, 4);
	SLTNode\* pos = SListFind(SList, 2);
	//SLTNode\* pos = SListFind(SList, 1);
	SListEraseBefore(&SList, pos);
	PrintList(SList);

	SLIstDestroy(&SList);
	PrintList(SList);

}
void test()
{
	int a = 'o';
	printf("%d\n", a);
}
int main(void)
{
	//TestSList2();
	//test1();
	//TestSList3();
	//TestSList4();
	//TestSList5();


![img](https://img-blog.csdnimg.cn/img_convert/c901e7b82121a86caf53b3398f90e6b8.png)
![img](https://img-blog.csdnimg.cn/img_convert/345086fc6671dbad6a8f72b52e78a38e.png)
![img](https://img-blog.csdnimg.cn/img_convert/12268641d0ac8420a4020c972379b53c.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**


	//SListInsertBefore(&SList, pos, 11);
	//PrintList(SList);

	SLTNode\* pos = SListFind(SList, 1);
	SListInsertBefore(&SList, pos, 8);
	PrintList(SList);
}
//删除之后的结点
void TestSList7()
{
	SLTNode\* SList = NULL;
	SListPushBack(&SList, 1);
	SListPushBack(&SList, 2);
	SListPushBack(&SList, 3);
	SListPushBack(&SList, 4);
	PrintList(SList);

	SLTNode\* pos = SListFind(SList, 2);
	SListEraseAfter(pos);
	PrintList(SList);
}
void TestSList8()
{
	SLTNode\* SList = NULL;
	SListPushBack(&SList, 1);
	SListPushBack(&SList, 2);
	SListPushBack(&SList, 3);
	SListPushBack(&SList, 4);
	PrintList(SList);

	//SLTNode\* pos = SListFind(SList, 4);
	SLTNode\* pos = SListFind(SList, 2);
	//SLTNode\* pos = SListFind(SList, 1);
	SListEraseBefore(&SList, pos);
	PrintList(SList);

	SLIstDestroy(&SList);
	PrintList(SList);

}
void test()
{
	int a = 'o';
	printf("%d\n", a);
}
int main(void)
{
	//TestSList2();
	//test1();
	//TestSList3();
	//TestSList4();
	//TestSList5();


[外链图片转存中...(img-Gqm9eD75-1715464246650)]
[外链图片转存中...(img-2VpCZv3c-1715464246651)]
[外链图片转存中...(img-43CWVaoe-1715464246651)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

  • 19
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值