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

SLTNode\* newnode = BuySLTNode(x);
if (\*pphead == NULL)
{
	\*pphead = newnode;
	//通过二级指针的解引用改变头结点【从NULL->newnode】
}
else
{
	SLTNode\* pptail = \*pphead;
	while (pptail->next)
	{
		pptail = pptail->next;
		//无需再使用二级指针,无需改变头结点,只需要做一个链接
		//改变结构体的指向,使用结构体指针
	}
	pptail->next = newnode;
}

}



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


![在这里插入图片描述](https://img-blog.csdnimg.cn/aec9e3bceb0a403e969f67c6626b8cab.jpeg#pic_center)  
 再来解释一下传参和链接的过程


* 可以看到函数中的形参我使用的是一个二级指针,然后在test中,我将这个指向头结点的一级指针SList的地址传了进去,当第一次尾插这个100的时候,便会进入第一个if分支,\*pphead便是解引用之后的一级指针,就相当于这个SList,因此当你将新开辟的结点newnode插入时,便直接将【\*pphead】指向了这块堆区中的地址。然后在第二次插入200的时候,便会进入第二个else分支,使用一个`一级指针类型的指针变量pptail`去接收【 \*pphead】,不断往后遍历,然后将两个结点通过【存放地址】的关系链接起来,就完成了我们的尾插操作
* 有同学可能对下面这段else逻辑比较疑惑,**为什么在修改头结点的时候要用到二级指针,但是在后面的链接中不需要用到了呢**❓,这个时候就要去思考,因为在后驱的结点链接时只是做一个将当前结点的next存放下一个结点在堆区中开辟的地址,这个操作我们前面的一级指针也可以完成,因此无需使用到二级指针。但是在外界传参的时候我们还是选择用`二级指针接收一级指针的地址`,以此使得函数内部和外部的指针都可以`指向堆区中的同一块空间`。而且对于函数的结构已经声明了是无法改变的,不能因为第一次需要改变头结点传入二级指针,但是在后面要无需用到二级指针就突然将这个pphead变为一级指针,这违背了编程的严谨性


接下去一样再通过函数栈帧的形式来讲解一下,加深印象,帮助理解


* 可以看到,这里将SList拷贝给了pphead,然后pphead指向这个值为100的结点了,但是呢SList并没有,这里的具体操作就是将SList这个一级指针的地址给到了二级指针pphead
* pphead中存地是SList的地址,这里的【\*pphead】解引用之后就是这个SList本身,然后主函数看到【SList = NULL】,所以在判断【\*pphead】的时候就相当于在判断【SList】,这才会进入if分支,通过让【`*pphead = newnode`】也让这个SList指向了100这个数据结点


![在这里插入图片描述](https://img-blog.csdnimg.cn/800b86046f0343fd811972bf3edcd83b.jpeg#pic_center)


* 然后随着PushBack函数的结束,函数所建立的栈帧也就被销毁,pphead和pptail就都没了,但此时这个SList却通过二级指针传参指向了尾插进去的第一个结点,**此时这个SList的头部结构才真正地被改变了**,可以继续尾插后面的结点


![在这里插入图片描述](https://img-blog.csdnimg.cn/9caead43bcb64f74b21642c5e9a4f8af.jpeg#pic_center)



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




---


### 2、尾删【Circuitous】



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


#### 经典错误案例分析


* 对于尾删我们一样从一个经典的错误案例开始说起



void SListPopBack(SLTNode* phead)
{
SLTNode* ptail = phead;
while (ptail->next)
{
ptail = ptail->next;
}
free(ptail);
tail = NULL;
}


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/60b26c48118c4a719d96f7ee531bbc72.jpeg#pic_center)


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/7ba970313c9746f496ddd6d286de7dc4.jpeg#pic_center)


#### 修改方式一:保存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;


![在这里插入图片描述](https://img-blog.csdnimg.cn/8b9b8beb2fcf4e74902e63f1591c4984.jpeg#pic_center)


#### 修改方式二: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;


![在这里插入图片描述](https://img-blog.csdnimg.cn/3de2945e49a741bc95be30b53323c69d.jpeg#pic_center)


#### 特殊情况修正【单个结点、二级指针修改、断言报错】


* 你以为用这样就可以真正地实现尾删了吗,那还远远不够,路还有很远🐎,继续分析下去吧
* 可以看到,在这里我执行了三次PopBack,然后到最后一次的时候,发现ptail->next==NULL,但是ptail->next->next却是一个**越界的位置**,这个时候其实就不对了,之前我们有说过,访问越界是一个很大的问题🈲



> 
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/2ea42dba6ceb4b379234dfde8cbe45a3.jpeg#pic_center)
> 
> 
> 


* 所以,对于单个结点的尾删,我们应该进行一个单独的判断。也就是当前传入进来然后接收的这个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它又来了



> 
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/2f51cad272fb4f9583bfb463ade8f89a.jpeg#pic_center)
> 
> 
> 


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



> 
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/8dcaf2989c0f4c0bb08a5b269ef08e27.jpeg#pic_center)
> 
> 
> 


* 那这下可好了,整个函数的结构又要重新定义,这其实是正常的,调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;
}
}


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


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


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



> 
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/d2c3e778501b4da4a3619718e3030a42.jpeg#pic_center)
> 
> 
> 


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



> 
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/87dc7bf7404e4ac58dadfc11b695b814.jpeg#pic_center)
> 
> 
> 


* 这其实就是什么问题?没错,就是访问越界的问题,当你将链表已经删空的时候,再去对链表进行一个删除,此时访问的就是一个随机的位置,超出了你所能访问的界限,编译器也就很好地为你【检查出了错误】
* 那此时我们应该怎么办呢?没错,就是在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域存放首个结点的地址,还是一样,对于头插和头删,都是要使用到二级指针的,否则内部的改动是无法带动外部的变化
* 要将这个二级指针化为一级指针我们说过很多遍,`一次*解引用即可`。链接上后将这个新的头所存在的地址给到指向头结点的指针即可


![在这里插入图片描述](https://img-blog.csdnimg.cn/fc13dc88009b416f83266638c4700666.jpeg#pic_center)



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


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


### 4、头删【Easy】


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/2597e9c1835244adb8507dac24d65b5b.jpeg#pic_center)  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/8b9d0276ffe84cecba746b14227010c0.jpeg#pic_center)



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”);
}


![在这里插入图片描述](https://img-blog.csdnimg.cn/8bb17792279440b1a9bd6a1e1171cdea.jpeg#pic_center)


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




---


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


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


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/d894cd07fe054efbb65a081d17834ed7.jpeg#pic_center)


* 可以看到,我们最后要达到的是这种效果,就是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


![在这里插入图片描述](https://img-blog.csdnimg.cn/9a121dc4482d44a3bc45e00f65f0c314.jpeg#pic_center)


#### 警惕传入空指针【✒细致讲解断言assert】


* 还是一样,继续思考有没有可能出现特殊情况,我们来看看这种
* 可以看到,链表中的结点值只有1~5但是却需要找一个88,很明显是找不到的,此时便会**return NULL**,那么外界的pos接受到的值就是一个空值,也就说这是一个空指针,所以当你传一个空指针进去,你让函数内部怎么实现一个插入呢



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


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/01245ed412234deb9caab21047a83ce8.jpeg#pic_center)


* 看到这个地方,我相信你的第一反应就是在函数一开始的加一个断言,判断这个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](https://bbs.csdn.net/topics/618545628)来看看,明确说到当表达式的值为0的时候,就会向标准的错误设备写入一条消息终止调用,也就是断言assert后面的语句均不会执行



> 
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/7cc49e32cb364650bf0ba9754921271d.jpeg#pic_center)
> 
> 
> 


* 所以应该在InsertAfter函数的开头加上这一句话



//0是假,非0才是真
assert(pos != NULL); //若不为空,则不会执行;若为空,则报出警告
//assert(pos);


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/41dc9ce604fe4d8b97931567ed7613b4.jpeg#pic_center)


#### 【生活小案例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就为链表的头结点**。直接调用一下我们上面写过的头插法即可,实现了功能的复用,具体图示如下👇  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/deb78d4ea16d4aacadbec32ad852423e.jpeg#pic_center)
* 然后再来说一下**普通情况**,也就是在链表其中的某一个位置前插入,所以我们`要获取pos指针所指向的前一个位置`,开头说到过,虽然单链表对于插入和删除很方便,但是访问结点并不方便,需要从头结点开始访问,于是还是一样的套路,定义一个cur指针首先获取到头结点指针的值,也就是把二级指针pphead解引用一下,然后一直去遍历即可,直到这个【`cur->next = pos`】为止停下来,表示当前cur所指向的结点所在的下一个位置便是pos,此时就可以做一个链接了,具体图示如下👇
* 可以看到,此时无需像在后面插入结点一般去考虑这个先后的顺序关系,只需要将【cur】【newnode】【pos】这三个指针做一个链接即可


![在这里插入图片描述](https://img-blog.csdnimg.cn/7b9d9137c4534a04ad3108997a29367f.jpeg#pic_center)


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/a827efc2e581402ba6dec9c7a8c2fc7a.jpeg#pic_center)


* 来看特殊情况


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


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


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


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



void SListEraseAfter(SLTNode* pos)


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/d76dc0416d284c618d8a1551dd4e2128.jpeg#pic_center)


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



pos->next = pos->next->next;
free(pos->next);


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/b7b5ce6bb4124a49913cfedab43b8a5f.jpeg#pic_center)


* 具体代码如下



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——酒店房门的钥匙🔑】


* 就我们理解而言,指针都是指向一个地址,野指针也不例外,它指向一个地址,但是呢,这块地址`不是确定的,而是随机的`。
* 这一块我不是非常了解,大家可以看看这篇博客——》[野指针的产生及其危害](https://bbs.csdn.net/topics/618545628)



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


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



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




---


* 看了野指针的危害,我们继续回到代码中来DeBug看看,使用野指针会出现什么情况



free(pos->next);
pos->next = pos->next->next;


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



> 
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/36c948d3caa74f46af1198eccda01ae2.jpeg#pic_center)  
>  ![在这里插入图片描述](https://img-blog.csdnimg.cn/7d84b8c4d4ba42d8a6b88a5ee1fcc56b.jpeg#pic_center)
> 
> 
> 


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/b2635c8012434974ba384f2d94225e41.jpeg#pic_center)


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/2c1f08cdbc2c4c5abcb800de5ae78716.jpeg#pic_center)


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


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


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



void SListEraseBefore(SLTNode** pphead, SLTNode* pos)


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


* 这种情况的话是无法删除的,因为头结点前面为空,所以直接返回即可,当然你也可以assert



//当pos就为头结点时
if ((*pphead) == pos) {
return;
}


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


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/1679959aef7f4bca99acf44ad289a47e.jpeg#pic_center)



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


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


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/826f78577c664d7eb18c9d26ff789647.jpeg#pic_center)


* 对于这种情况,因为我们是要去删除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);
}

}


* 我们来测试一下看看



> 
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/4130d4f58f3344d595db640c0d39a907.jpeg#pic_center)  
>  ![在这里插入图片描述](https://img-blog.csdnimg.cn/3126f4f9b10740b78be665c00cadd54b.jpeg#pic_center)  
>  ![在这里插入图片描述](https://img-blog.csdnimg.cn/b26f8441116045d88f1901460d980c17.jpeg#pic_center)
> 
> 
> 


### 10、释放链表


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/03c34ce006f74184b8fb9448727bb259.jpeg#pic_center)


* 看一下代码



void SLIstDestroy(SLTNode** pphead)
{
SLTNode* cur = *pphead;

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

	cur = nextNode;
}

}


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/bc6f49c8230c427b85da83789f6b266b.jpeg#pic_center)


* 是的,可以看到又是我们所熟悉的野指针,为什么呢?因为你将链表的头结点释放了,那这就是一块随机的地址,上面说到过,访问一块随机的地址,也就形成了【野指针】


#### 【生活小案例5——利剑不锋利🗡】


* 上面又说到了这个野指针的问题,我们再来谈一谈,对于野指针,也是它就像是一把锋利的剑一样,非常危险,但是呢,它又不是随时都会有危险,因为当这把剑放在剑鞘中时,其实是非常安全的,并不是伤害到你,但是当你将它把出来的时候,那这个时候你就要小心使用了,一不留神可能就会伤到自己。
* 对于野指针也是一样,有的时候你知道这个指针可能会是野指针,但是你不去使用它访问数据,那其实是很安全的,不会有问题,但是当你使用到了这个野指针去访问的时候,其实就会非常危险了。所以在这里还是和大家说一句:谨慎使用指针




---


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


![在这里插入图片描述](https://img-blog.csdnimg.cn/4a0c773b081049cbadf17b155242c99d.jpeg#pic_center)




---


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


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);

img
img

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

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

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

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);

[外链图片转存中…(img-dxzeEjEG-1714802072248)]
[外链图片转存中…(img-vFjrJ6DB-1714802072248)]

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值