单链表的实现(全注释promax版)

news2024/11/14 18:49:10

目录

前言:

哨兵位:

链表的概念

链表的相关操作:

链表的创建:

打印链表:

申请新节点:

链表的尾插:

 !!!对于传参中二级指针的解释:

链表的头插:

链表的尾删: 

链表的头删:

寻找结点:

在链表的指定位置前插入:

在链表的指定位置后插入:

删除pos位置的结点:

删除pos位置后的结点:

销毁链表:

最终结果:

SList.h文件:

SList.c文件:

test.c文件:


前言:

建议哈,等把其它内容看完后再来看前言......

链表一共有八种结构:

  • 带头单向不循环/循环链表
  • 带头双向不循环/循环链表
  • 不带头单向不循环/循环链表
  • 不带头双向不循环/循环链表

但是我们实际中 最常⽤还是两种结构 不带头单向不循环链表 带头双向循环链表
⽆头单向⾮循环链表:
        结构简单,⼀般不会单独⽤来存数据。更多是作为其他数据结构的⼦结构
带头双向循环链表:
        结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构就是它

后续使用二级指针pphead的原因:

请看下面关于尾插的调试:

	//假设令plist指向第一个有效结点(在那之前先让它为空......)
    SLNode* plist = NULL;	
	//尾插
	SLPushBack(plist, 1);  
    SLPushBack(plist, 2);  
	//我们在后续使用时用的是这样的SLPushBack(&plist, 1);  
	SLPrint(plist);

未使用二级指针时进行第二次两次插入前plist的值情况:

我们发现此时plist的值为空?pphead为什么返回不回去呢

使用二级指针并完成更改后的结果:

再次插入时plist中的情况:

此时pphead两次插入时的值已经返回去了

结论: 感觉就是传值调用和传址调用的区别,第一种情况是传值调用pphead只是形参它是实参的一个拷贝对它的改变影响不了实参它所保留的值在出函数时就会被销毁(看图片你也可以发现它的地址与&plist的地址不一样),第二种情况时传址调用*pphead获得的是实参的地址:

这里你会发现二级指针pphead的地址就会与plist的地址一样了,对pphead的操作其实就是对plist的操作,这就是传址调用的妙处。

这是我在一个小时前想写的话:

我也是初学者对于这些内容也有点懵,描述的也不算清楚但是应该会有一定帮助

现在我想写的是:

ok搞定 (之前一直纠结哨兵位对是否使用二级指针的影响,现在看来好像没那个必要啊,到底使用了哨兵位后有啥好处我在这里考虑什么呢?)

链表的具体概念

链表是线性表的一种,它就相当于一列火车:

        而链表这辆火车中的“车厢”,我们称之为“ 结点/节点 ”。就像普通车厢中需要存放数据一样,链表中的每个结点也要存储数据,但是与普通车厢可以从头一路走到尾的情况不同的是,链表中的每个"车厢"(结点)在 物理逻辑上不连续,在虚拟逻辑上连续。
对没错就是你想的那个物理和虚拟
结点 当前节点中保存的数据data + 保存下⼀个节点地址的指针变量next(单链表的情况)
为什么需要指针变量来保存下⼀个节点的位置?
        因为链表在内存空间上的存储是非连续的 ,就和火车车厢一样,根据需求进行增加和删除。通俗来讲就是,用到你这节”车厢“时把指向你这节“车厢”中的next指针(火车挂钩)指向你的下一节"车厢",不用你的时候就把你这节”车厢“中的next指针(火车挂钩)置空,你就一边闲着去。

不带头单向不循环链表的相关操作:

链表的创建:

!!需要补充的是:*pphead存储的值一直是链表的第一个有效结点的地址!!

!还有就是它是一个SLNode*类型的指针变量!

!!还有如果一个指针指向一个结点那么该指针中存放的就是该结点的地址!!

创建链表需要经历以下操作:

1、定义一个结构体来表示链表的结点(SList.h文件)

//定义一种链表节点的结构(实际应用中有多种,这里只演示最基本的结构)
typedef int SLDataType; //便于切换链表中存储数据的类型
struct SListNode {   
	SLDataType data;  //存储数据
	struct SListNode* next;  //用来保存下一个节点地址的指针变量next
};
typedef struct SListNode SLNode;  //将链表结点的结构体重命名为SLNode
2、创建链表的函数(第二点这里 只是为了方便理解后续内容,具体情况请看创建后的实际操作)
该函数主要进行的操作是:        
①创建新的有效结点并为其开辟内存空间,同时用一个指针node指针指向该有效结点的空间
②将新有效结点的next指针指向下一个结点(node里面存储的就是下一个结点的地址)
//申请结点函数
void slttest()
{
	SLNode* node1 = (SLNode*)malloc(sizeof(SLNode)); 
	node1->data = 1;
	SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
	node2->data = 2;
	SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
	node3->data = 3;
	SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	//打印链表
	SLNode* plist = node1;  
	SLPrint(plist);
}

打印链表:

//用phead表示指向第一个有效结点的指针
void SLPrint(SLNode* phead) 
{	
	//循环打印
	//令pcur指针同样指向第一个有效结点的地址(把它当作phead的拷贝)
	SLNode* pcur = phead;
    //当pcur指向的地址不为空时继续循环
	while (pcur)
	{
    //打印此时所指向结点中的数据
		printf("%d ->", pcur->data);
    //打印结束后让pcur指向下一个结点的地址
		pcur = pcur->next;
	}
    //打印完成后,用NULL在小黑框中标识一下证明此时链表已经打印完了
	printf("NULL\n");
}

申请新节点:

//申请有效结点,并在该结点中存储数据x
SLNode* SLByNode(SLDataType x) 
{
    //为有效结点申请一个新的空间,并用node指针存储该空间的地址(用node指向该空间)
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
    //该节点中存储的数据为x
	node->data = x;
    //将该结点的下一个结点置为空,因为我们也不知道它后面到底还要不要结点了
	node->next = NULL;
    //返回申请的有效结点地址(返回值为node,而node里面存储的就是有效结点所在内存空间的地址)
	return node;
}

链表的尾插:

//链表的尾插
void SLPushBack(SLNode** pphead, SLDataType x)
{
    //想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);

	SLNode* node = SLByNode(x);
    //如果没有第一个有效结点(链表为空)那就让*pphead指向创建的有效结点的地址(让该结点作为链表的第一个有效结点)
	if (*pphead == NULL)
	{
		*pphead = node;//(注意=的意思是赋值,而node的值就是有效结点的地址,把有效结点的地址传递给*ppead那么此时它里面存储的值就是有效结点的地址,此时它就相当于node了)
		return;
	}
    //如果有第一个有效结点,则通过循环读取至链表的结尾
	SLNode* pcur = *pphead;//(赋值原理同上)
    //然后利用pcur->next遍历至链表的末尾
	while (pcur->next)
	{
		pcur = pcur->next;//(赋值原理同上)
	}
   //当遍历至链表的末尾时,开始执行插入操作
	pcur->next = node;//(赋值原理同上)
    //还是解释一下:将有效结点的地址交给当前pcur指向结点中的next指针
}

注意在理解这些操作时,一定要清楚的是”=“的作用就是赋值(感觉理解这点很重要)

链表的头插:

//链表的头插
void SLPushFront(SLNode** pphead, SLDataType x)//相当于两个互相赋值
{
    //想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
	SLNode* node = SLByNode(x);
    //下面两条进行的其实就是简单的交接工作
	//因为是头插,所以此时要将原来第一个有效结点的地址交给要头插新的有效结点的next指针
	node->next = *pphead;
	//然后将要插入的有效结点的地址交给*pphead(因为*pphead中存放的一定是第一个有效结点的地址,而这里我们将链表的第一个有效结点的地址变为了头插的新的有效结点的地址所以要将*pphead中存储的地址改变一下)
	*pphead = node;
}

链表的尾删: 

//链表的尾删(链表为空的情况下不能尾删)
void SLPopBack(SLNode** pphead)
{    
	//想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
    //判断第一个有效结点是否存在(表为空不能进行尾删)
	assert(*pphead);

	//当有且只有一个有效结点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
    else
    {
    //当不止一个有效结点时,找尾结点和尾结点的前一个结点
    //这是为了防止尾删后它前一个结点的next指针变为野指针,我们要在删除尾结点后将该next指针置为空
    //定义prev存放尾结点的前一个结点的地址(暂时将该指针的值置为空)
	SLNode* prev = NULL;
    //定义ptail为用于找尾结点的指针,先让它接收第一个有效结点的地址
	SLNode* ptail = *pphead;
    //当ptail->next的值为空时,证明上一次循环时ptail已经指向了尾结点
	while (ptail->next != NULL)
	{
        //用prev将ptail最后一次循环前的值保存下来,不存储时就没法找到NULL前的一个结点(尾结点)    
        //此时prev保存的就是尾结点的前一个结点的地址
		prev = ptail;
		ptail = ptail->next;
	}
	//这里处理的就是野指针的情况,循环结束后ptail->text的值就为空了,将它赋值给尾结点上一个结点的next指针
	prev->next = ptail->next;
    //下面两步的操作的具体解释放在后面了......
	free(ptail);
	ptail = NULL;
    }
}

这是只有一个有效结点时尾删操作前*pphead的值 :

这是free(*pphead)后的结果:(这里地址不会变的哈只是这是两次调试的结果看data和next就行)

这是将*pphead置为空的结果: 

        我们可以发现free后只是将申请的内存空间释放掉了,但是*pphead中仍然保存了一个地址,那这个地址对应的内存空间已经被释放了,如果我们还要利用这个地址做一些事情,这时*pphead就是野指针了,所以此时就需要将*pphead置为空 

其实对于free(*pphead)不懂的,你只需要知道它是一个SLNode*类型的指针变量就可以理解了 

        emm,其实看注释就够了下面的图中的文字部分可能会与注释内容有冲突,一切以注释为主,当然图中除了文字部分的内容还是可以多看一看的...... 

链表的头删:

//链表的头删
void SLPopPront(SLNode** pphead)
{
	//想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
    //判断第一个有效结点是否存在(表为空不能进行尾删)
	assert(*pphead);

	//当有且只有一个有效结点时(具体的从这里开始便不再解释,不懂的请回去看尾删~)
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
   
	//令del指针存放第一个有效结点的地址
	SLNode* del = *pphead;
	//因为要环头了(第一个有效结点),此时第二个有效结点要作为第一个有效结点所以将原来第一个有效结点的next指针中存储的地址(也就是第二个有效结点的地址)交给*pphead
	*pphead = (*pphead)->next;
	//将del存储的原第一个有效结点的内存空间释放
	free(del);
    //虽然内存空间释放但是del仍然保存了一个地址如果不将del置为空那么它就是野指针
	del = NULL;
}

寻找结点:

//查找结点(前面的几个操作你懂了这里的代码应该就不用解释了吧)
SLNode* SLFind(SLNode** pphead, SLDataType x)
{
	//想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

//该函数需要与在指定位置插入删除结合,返回的结果使用一个指针来接收,在test.c文件中的使用情况如下:
SLNode* find = SLFind(&plist,2);//查找数据为2的结点
SLInsert(&plist,find,x)//在find(数据为2)的结点前插入含有数据x的新节点

在完成以下代码后需要考虑的三种情况:

1、pos是头结点

2、pos是中间结点

3、pos是最后一个结点

在链表的指定位置前插入:

//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
    //想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
    //判断第一个有效结点是否存在(表为空不能进行尾删)
	assert(*pphead);
    //你得确定能找到你说的那个结点,你都找不到那你怎么在它之前插入结点啊哥们...
	assert(pos);

	SLNode* node = SLByNode(x);

	//有且只有一个有效结点,此时在第一个有效结点前进行插入操作就相当于头插
	if(pos == *pphead)
	{
		node->next = *pphead;
		*pphead = node;
		return;
	}

	//当不只有一个有效结点的时候,先通过循环找到指向pos前一个结点的地址(prev指向的结点)
	SLNode* prev = *pphead; 

    //当prev->next指向pos的时候跳出循环
	while (prev->next != pos)
	{
		prev = prev->next;
	}
    //此时prev指向pos位置前的一个结点(prev中存放的就是该结点的地址)

    //最后,处理插入位置两边的结点与新结点三者之间的关系prve node pos
    //此时下面的两个操作顺序可以交换
	node->next = pos;
	prev->next = node;
}

在链表的指定位置后插入:

//在指定位置之后插入数据(这里就不需要传一个二级指针了)
void SLInsertAfter(SLNode* pos, SLDataType x)
{
    //你得确定能找到你说的那个结点,你找不到那你怎么在它之后插入结点啊
	assert(pos);
	SLNode* node = SLByNode(x);
	//处理pos node pos->next三者之间的关系(看...看图......)

	node->next = pos->next;
	pos->next = pos;
}

//使用案例:
//SLNode* find = SLFind(&plist,1);
//SLInsertAfter(find,100);

        虽然看着跟前面在指定位置之前插入的图看起来差不多,但是在指定位置后插入省去了用循环寻找pos的上一个结点的步骤更好理解

删除pos位置的结点:

//删除pos结点
void SLErase(SLNode** pphead, SLNode* pos)
{
    //想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
    //判断第一个有效结点是否存在(表为空不能进行尾删)
	assert(*pphead);
    //还是那句话:你得确定能找到你说的那个结点,你都找不到那你怎么在它之前插入结点啊哥们.....
	assert(pos);
    
    //当pos为第一个有效结点时
	if (pos == *pphead)
	{
        //第一个有效结点要换人了,换人前先把遗嘱交代了哈(传地址)
		*pphead = (*pphead)->next;
		free(pos);
		return;
	}
    //当pos不为第一个有效结点时
	//先找到pos的前一个结点,然后(后续内容与之前的操作类似)
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
    //先完成pos两边结点的交接工作,然后再释放pos结点
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

删除pos位置后的结点:

//删除pos结点之后的数据
void SLEraseAfter(SLNode* pos)
{
    //除了pos不为空以外,还需要pos->next不为空,因为pos刚好是最后一个结点你总不能删除一个NULL
	assert(pos && pos->next);
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
    del=NULL;
}

销毁链表:

//销毁链表
void SLDestroy(SLNode** pphead)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	//循环删除
	while (pcur)
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
    //此时链表所有的有效结点的内存空间已经释放了,*pphead再保存着已经释放的内存空间的地址是不是不太合适?(野指针)
	*pphead = NULL;
}

最终结果:

SList.h文件:

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

//定义链表节点的结构
typedef int SLDataType;
struct SListNode {   //定义一个表示链表节点的结构体
	SLDataType data;  //链表中用于存储数据的成员(某个节点的数据)
	struct SListNode* next;  //用来保存下一个节点地址的指针变量next
};
typedef struct SListNode SLNode;  //将指向下一个节点的指针类型重命名为SLNode

//创建几个结点组成的链表,并打印链表
void SLPrint(SLNode* phead);  

//链表的尾插
void SLPushBack(SLNode** phead, SLDataType x);

//链表的头插
void SLPushFront(SLNode** phead, SLDataType x);

//链表的尾删
void SLPopBack(SLNode** pphead);

//链表的头删
void SLPopPront(SLNode** pphead);

//找结点
SLNode* SLFind(SLNode** pphead,SLDataType x);

//链表的在指定位置之前插入
void SLInsert(SLNode** phead, SLNode* pos,SLDataType x);

//链表的指定位置删除
void SLInsertAfter(SLNode* pos, SLDataType x);

//删除pos位置的结点
void SLErase(SLNode** pphead, SLNode* pos);

//删除pos后的结点
void SLEraseAfter(SLNode* pos);

//销毁链表
void SLDestroy(SLNode** pphead);

SList.c文件:

#include "SList.h"
//用phead表示指向第一个有效结点的指针
void SLPrint(SLNode* phead) 
{	
	//循环打印
	//令pcur指针同样指向第一个有效结点的地址(把它当作phead的拷贝)
	SLNode* pcur = phead;
    //当pcur指向的地址不为空时继续循环
	while (pcur)
	{
    //打印此时所指向结点中的数据
		printf("%d ->", pcur->data);
    //打印结束后让pcur指向下一个结点的地址
		pcur = pcur->next;
	}
    //打印完成后,用NULL在小黑框中标识一下证明此时链表已经打印完了
	printf("NULL\n");
}
//申请有效结点,并在该结点中存储数据x
SLNode* SLByNode(SLDataType x) 
{
    //为有效结点申请一个新的空间,并用node指针存储该空间的地址(用node指向该空间)
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
    //该节点中存储的数据为x
	node->data = x;
    //将该结点的下一个结点置为空,因为我们也不知道它后面到底还要不要结点了
	node->next = NULL;
    //返回申请的有效结点地址(返回值为node,而node里面存储的就是有效结点所在内存空间的地址)
	return node;
}

//链表的尾插
void SLPushBack(SLNode** pphead, SLDataType x)
{
    //想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);

	SLNode* node = SLByNode(x);
    //如果没有第一个有效结点(链表为空)那就让*pphead指向创建的有效结点的地址(让该结点作为链表的第一个有效结点)
	if (*pphead == NULL)
	{
		*pphead = node;//(注意=的意思是赋值,而node的值就是有效结点的地址,把有效结点的地址传递给*ppead那么此时它里面存储的值就是有效结点的地址,此时它就相当于node了)
		return;
	}
    //如果有第一个有效结点,则通过循环读取至链表的结尾
	SLNode* pcur = *pphead;//(赋值原理同上)
    //然后利用pcur->next遍历至链表的末尾
	while (pcur->next)
	{
		pcur = pcur->next;//(赋值原理同上)
	}
   //当遍历至链表的末尾时,开始执行插入操作
	pcur->next = node;//(赋值原理同上)
    //还是解释一下:将有效结点的地址交给当前pcur指向结点中的next指针
}

//链表的头插
void SLPushFront(SLNode** pphead, SLDataType x)//相当于两个互相赋值
{
    //想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
	SLNode* node = SLByNode(x);
    //下面两条进行的其实就是简单的交接工作
	//因为是头插,所以此时要将原来第一个有效结点的地址交给要头插新的有效结点的next指针
	node->next = *pphead;
	//然后将要插入的有效结点的地址交给*pphead(因为*pphead中存放的一定是第一个有效结点的地址,而这里我们将链表的第一个有效结点的地址变为了头插的新的有效结点的地址所以要将*pphead中存储的地址改变一下)
	*pphead = node;
}

//链表的尾删(链表为空的情况下不能尾删)
void SLPopBack(SLNode** pphead)
{    
	//想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
    //判断第一个有效结点是否存在(表为空不能进行尾删)
	assert(*pphead);

	//当有且只有一个有效结点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
    else
    {
    //当不止一个有效结点时,找尾结点和尾结点的前一个结点
    //这是为了防止尾删后它前一个结点的next指针变为野指针,我们要在删除尾结点后将该next指针置为空
    //定义prev存放尾结点的前一个结点的地址(暂时将该指针的值置为空)
	SLNode* prev = NULL;
    //定义ptail为用于找尾结点的指针,先让它接收第一个有效结点的地址
	SLNode* ptail = *pphead;
    //当ptail->next的值为空时,证明上一次循环时ptail已经指向了尾结点
	while (ptail->next != NULL)
	{
        //用prev将ptail最后一次循环前的值保存下来,不存储时就没法找到NULL前的一个结点(尾结点)    
        //此时prev保存的就是尾结点的前一个结点的地址
		prev = ptail;
		ptail = ptail->next;
	}
	//这里处理的就是野指针的情况,循环结束后ptail->text的值就为空了,将它赋值给尾结点上一个结点的next指针
	prev->next = ptail->next;
    //下面两步的操作的具体解释放在后面了......
	free(ptail);
	ptail = NULL;
    }
}

//链表的头删
void SLPopPront(SLNode** pphead)
{
	//想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
    //判断第一个有效结点是否存在(表为空不能进行尾删)
	assert(*pphead);

	//当有且只有一个有效结点时(具体的从这里开始便不再解释,不懂的请回去看尾删~)
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
   
	//令del指针存放第一个有效结点的地址
	SLNode* del = *pphead;
	//因为要环头了(第一个有效结点),此时第二个有效结点要作为第一个有效结点所以将原来第一个有效结点的next指针中存储的地址(也就是第二个有效结点的地址)交给*pphead
	*pphead = (*pphead)->next;
	//将del存储的原第一个有效结点的内存空间释放
	free(del);
    //虽然内存空间释放但是del仍然保存了一个地址如果不将del置为空那么它就是野指针
	del = NULL;
}

//查找结点
SLNode* SLFind(SLNode** pphead, SLDataType x)
{
	//判断传入的头结点plist是否为空
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

//该函数需要与在指定位置插入删除结合,返回的结果使用一个指针来接收,在test.c文件中的使用情况如下:
//SLNode* find = SLFind(&plist, 2);//查找数据为2的结点
//SLInsert(&plist, find, x)//在find(数据为2)的结点前插入含有数据x的新节点

//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
    //想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
    //判断第一个有效结点是否存在(表为空不能进行尾删)
	assert(*pphead);
    //你得确定能找到你说的那个结点,你都找不到那你怎么在它之前插入结点啊哥们...
	assert(pos);

	SLNode* node = SLByNode(x);

	//有且只有一个有效结点,此时在第一个有效结点前进行插入操作就相当于头插
	if(pos == *pphead)
	{
		node->next = *pphead;
		*pphead = node;
		return;
	}

	//当不只有一个有效结点的时候,先通过循环找到指向pos前一个结点的地址(prev指向的结点)
	SLNode* prev = *pphead; 

    //当prev->next指向pos的时候跳出循环
	while (prev->next != pos)
	{
		prev = prev->next;
	}
    //此时prev指向pos位置前的一个结点(prev中存放的就是该结点的地址)

    //最后,处理插入位置两边的结点与新结点三者之间的关系prve node pos
    //此时下面的两个操作顺序可以交换
	node->next = pos;
	prev->next = node;
}

//在指定位置之后插入数据(这里就不需要传一个二级指针了)
void SLInsertAfter(SLNode* pos, SLDataType x)
{
    //你得确定能找到你说的那个结点,你找不到那你怎么在它之后插入结点啊
	assert(pos);
	SLNode* node = SLByNode(x);
	//处理pos node pos->next三者之间的关系(看...看图......)

	node->next = pos->next;
	pos->next = pos;
}

//使用案例:
//SLNode* find = SLFind(&plist,1);
//SLInsertAfter(find,100);

//使用案例:
//SLNode* find = SLFind(&plist,1);
//SLInsertAfter(find,100);

//删除pos结点
void SLErase(SLNode** pphead, SLNode* pos)
{
    //想要进行链表的插入和删除操作就必须使用一个二级指针pphead
	assert(pphead);
    //判断第一个有效结点是否存在(表为空不能进行尾删)
	assert(*pphead);
    //还是那句话:你得确定能找到你说的那个结点,你都找不到那你怎么在它之前插入结点啊哥们.....
	assert(pos);
    
    //当pos为第一个有效结点时
	if (pos == *pphead)
	{
        //第一个有效结点要换人了,换人前先把遗嘱交代了哈(传地址)
		*pphead = (*pphead)->next;
		free(pos);
		return;
	}
    //当pos不为第一个有效结点时
	//先找到pos的前一个结点,然后(后续内容与之前的操作类似)
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
    //先完成pos两边结点的交接工作,然后再释放pos结点
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

//删除pos结点之后的数据
void SLEraseAfter(SLNode* pos)
{
    //除了pos不为空以外,还需要pos->next不为空,因为pos刚好是最后一个结点你总不能删除一个NULL
	assert(pos && pos->next);
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
    del=NULL;
}

//销毁链表
void SLDestroy(SLNode** pphead)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	//循环删除
	while (pcur)
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
    //此时链表所有的有效结点的内存空间已经释放了,*pphead再保存着已经释放的内存空间的地址是不是不太合适?(野指针)
	*pphead = NULL;
}

test.c文件:

#include "SList.h"
//申请结点函数
//void slttest()
//{
//	//使用malloc函数动态分配,创建链表的头节点,它不包含任何数据,知识用来指向链表的第一个实际节点
//	SLNode* node1 = (SLNode*)malloc(sizeof(SLNode)); 
//	//head
//	node1->data = 1;
//	SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
//	node2->data = 2;
//	SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
//	node3->data = 3;
//	SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));
//	node4->data = 4;
//
//	//实现四个节点的链接
//	//初始化头节点的next指针为node2指针变量
//	node1->next = node2;
//	node2->next = node3;
//	node3->next = node4;
//	node4->next = NULL;
//
//	//打印链表
//	SLNode* plist = node1;  //定义一个SLNode*类型的指针变量plist,他也叫头指针,我们用它指向链表的头节点
//	
//	//注意头节点和头指针的概念是不同的:
//	/*在链表的上下文中,通常将链表的第一个节点称为头节点(Head Node),但是头节点和头指针(Head Pointer)是不同的概念。
//	头节点是链表中的第一个实际节点,它包含数据和指向下一个节点的指针。头节点是链表的起始点,它可以存储实际的数据,也可以只是一个占位符节点,不存储实际的数据。
//	头指针是指向链表的头节点的指针。它是一个指针变量,存储着头节点的地址。通过头指针,我们可以访问链表中的每个节点,或者进行其他链表操作。
//	因此,头节点是链表中的一个节点,而头指针是指向头节点的指针。它们是不同的概念,但在某些情况下,人们可能会将它们混用或将它们视为相同的概念,因为头节点通常通过头指针来访问。*/
//	
//	SLNPrint(plist);
//}

void slttest()
{

	SLNode* plist = NULL;	
	//尾插
	SLPushBack(&plist, 1);  
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);//1->2->3->4->NULL
	SLPrint(plist);
	头插
	//SLPushFront(&plist, 1);
	//SLPushFront(&plist, 2);
	//SLPushFront(&plist, 3);
	//SLPushFront(&plist, 4);//4->3->2->1->NULL
	//SLPrint(plist);
	//尾删
	SLPopBack(&plist);
	SLPopBack(&plist);
	SLPopBack(&plist);
	头删
	//SLPopPront(&plist);
	//SLPopPront(&plist);
	//SLPopPront(&plist);
	//SLPopPront(&plist);
	//SLPopPront(&plist);
	//SLPopPront(&plist);
	指定位置插入
	//SLNode* find = SLFind(&plist, 4);
	//SLInsert(&plist, find,11);//1->11->2->3->4->NULL
	在指定位置之后插入数据
	//SLInsertAfter(find, 100);
	删除pos位置的节点
	//SLErase(&plist, find);//1->2->3->NULL
	删除pos之后的节点
	//SLEraseAfter(find);
	//
	//销毁链表
	//SLDestory(&plist);
	//检验是否成功销毁
	SLPrint(plist);
}

int main()
{
	slttest();
	return 0;
}

注意事项:

1、判断条件的等号都是==

2、冒号是否写了

3、函数或者指针变量的名字是否书写正确 

4、最后的test.c文件实验时可能会存在一些多删之类的问题(函数写多了)请自行检查~

看完这个对指针的理解和使用又深了一层...... 

~over~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1122113.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决使用WebTestClient访问接口报[185c31bb] 500 Server Error for HTTP GET “/**“

解决使用WebTestClient访问接口报[185c31bb] 500 Server Error for HTTP GET "/**" 问题发现问题解决 问题发现 WebTestClient 是 Spring WebFlux 框架中提供的用于测试 Web 请求的客户端工具。它可以不用启动服务器&#xff0c;模拟发送 HTTP 请求并验证服务器的响…

【UCAS自然语言处理作业一】利用BeautifulSoup爬取中英文数据,计算熵,验证齐夫定律

文章目录 前言中文数据爬取爬取界面爬取代码 数据清洗数据分析实验结果 英文数据爬取爬取界面动态爬取 数据清洗数据分析实验结果 结论 前言 本文分别针对中文&#xff0c;英文语料进行爬虫&#xff0c;并在两种语言上计算其对应的熵&#xff0c;验证齐夫定律github: ShiyuNee…

Java Static关键字 单例设计模式

类变量 类变量&#xff08;静态变量&#xff09;&#xff1a;有static修饰&#xff0c;属于类&#xff0c;在计算机中只有一份&#xff0c;被类的所有对象共享 可以通过类名访问&#xff0c;也可以通过对象名访问&#xff0c;但是推荐用类名访问类变量一般用public修饰&#xf…

Maven 生命周期clean default size含义

clean 负责清理工作&#xff0c;清理上一次项目构建产生的一些文件&#xff0c;如编译后的字节码文件&#xff0c;打包后的jar包文件 default 整一个项目构建的核心工作&#xff0c;如编译&#xff0c;测试&#xff0c;打包&#xff0c;安装&#xff0c;部署等等 size 生成报告…

springBoot--web--函数式web

函数式web 前言场景给容器中放一个Bean&#xff1a;类型是 RouterFunction<ServerResponse>每个业务准备一个自己的handler使用集合的时候加注解请求的效果 前言 springmvc5.2 以后允许我们使用函数式的方式&#xff0c;定义web的请求处理流程 函数式接口 web请求处理的…

Docker开箱即用,开发码农加分项部署技术拿下!

目录 Docker概述 效果呈现 镜像 & 镜像仓库 & 容器 镜像 DockerHub 配置国内源加速 容器 简单的命令解读 Docker基础 常用命令 案例演示 数据卷 什么是数据卷 数据卷命令 演示环节 匿名数据卷 案例演示 自定义挂载位置 案例演示 自定义镜像 镜像结构 Dockerfile …

Python —— UI自动化之Page Object模式

1、Page Object模式简介 1、二层模型 Page Object Model&#xff08;页面对象模型&#xff09;, 或者也可称之为POM。在UI自动化测试广泛使用的一种分层设计 模式。核心是通过页面层封装所有的页面元素及操作&#xff0c;测试用例层通过调用页面层操作组装业务逻辑。 1、实战 …

Kafka简单入门01

目录 Kafka的核心组件 Kafka的分区有序 Kafka的多副本机制 Kafka的安装部署 Kafka的基本操作 Apache Kafka 是一个高吞吐量的分布式流数据平台&#xff0c;通常用于构建实时数据流处理应用程序。 Kafka的核心组件 主题&#xff08;Topic&#xff09;&#xff1a;主题是 K…

【vSphere 8 自签名 VMCA 证书】企业 CA 签名证书替换 vSphere VMCA CA 证书Ⅰ—— 生成 CSR

目录 替换拓扑图证书关系示意图说明 & 关联博文1. 默认证书截图2. 使用 certificate-manager 生成CSR2.1 创建存放CSR的目录2.2 记录PNID和IP2.3 生成CSR2.4 验证CSR 参考资料 替换拓扑图 证书关系示意图 本系列博文要实现的拓扑是 说明 & 关联博文 因为使用企业 …

为什么不建议使用Python自带的logging?

B站|公众号&#xff1a;啥都会一点的研究生 包括我在内的大多数人&#xff0c;当编写小型脚本时&#xff0c;习惯使用print来debug&#xff0c;肥肠方便&#xff0c;这没问题&#xff0c;但随着代码不断完善&#xff0c;日志功能一定是不可或缺的&#xff0c;极大程度方便问题…

【CSS】使用 CSS 实现一个宽高自适应的正方形

1. 利用 padding 或 vw <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><metaname"viewport"content"widthdevice-width, initial-scale1.0"><title>Document</title><st…

带你深入了解微信小程序【授权登录】

&#x1f3c5;我是默&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;在这里&#xff0c;我要推荐给大家我的专栏《微信小程序 》。&#x1f3af;&#x1f3af; &#x1f680;无论你是编程小白&#xff0c;还是有一定基础的程序员&#xff0c;…

FPGA的斐波那契数列Fibonacci设计verilog,代码和视频

名称&#xff1a;斐波那契数列Fibonacci设计verilog 软件&#xff1a;Quartus 语言&#xff1a;Verilog 代码功能&#xff1a; 设计一个产生斐波那契数列&#xff08;也叫黄金分割数列&#xff09;的硬件电路: 斐波那契数列中每个数为其相邻前两个数的和:即FNFN1FN2,(数列…

Node学习笔记之包管理工具

一、概念介绍 1.1 包是什么 『包』英文单词是package &#xff0c;代表了一组特定功能的源码集合 1.2 包管理工具 管理『包』的应用软件&#xff0c;可以对「包」进行 下载安装 &#xff0c; 更新 &#xff0c; 删除 &#xff0c; 上传 等操作 借助包管理工具&#xff0c;可…

windows系统如何查看Linux文件系统中的图片缩略图

背景 使用autoDL云算力资源&#xff0c;没有GUI界面&#xff0c;实验结果都是图片为主&#xff0c;为了可视化查看图片&#xff0c;所以萌生了是否有软件可以查看Linux文件系统中的图片缩略图。 目前网上有几种方法可以支持在windows系统上查看Linux文件系统中的图片缩略图&a…

逻辑漏洞详解

原理&#xff1a; 没有固定的概念&#xff0c;一般都是不符合常识的情况。比如任意用户注册&#xff0c;短信炸弹&#xff0c;占用资源&#xff0c;交易支付、密码修改、密码找回、越权修改、越权查询、突破限制。 根据实际业务逻辑进行比对&#xff0c;购物的可以根据数量&a…

【Spring Boot 源码学习】HttpEncodingAutoConfiguration 详解

Spring Boot 源码学习系列 HttpEncodingAutoConfiguration 详解 引言往期内容主要内容1. CharacterEncodingFilter2. HttpEncodingAutoConfiguration2.1 加载自动配置组件2.2 过滤自动配置组件2.2.1 涉及注解2.2.2 characterEncodingFilter 方法2.2.3 localeCharsetMappingsCus…

商品规格项数据的遍历以及添加

简介 今天在处理规格项的数据时遇到了一些问题&#xff0c;接下来就给大家分享一下 规格项数据设计 "specifications": [{"goodsSpecificationId": 6,"goodsSpecificationName": "网络类型","goodsTypeId": 24,"goods…

【Acwing166】数独(dfs+剪枝+位运算)

本题思路来源于acwing算法提高课 题目描述 看本文需要准备的知识 1.dfs算法基本思想 2.位运算基础 3.对剪枝这个名词的大概了解 剪枝优化位运算优化 常见四种剪枝策略 首先考虑这道题的搜索顺序&#xff0c;很明显&#xff0c;可以随意选择一个空格子&#xff0c;分支为这…

《广西开放大学电大搜题微信公众号——探索学习的新途径》

随着社会的发展&#xff0c;教育无疑成为了人们的基本需求之一。然而&#xff0c;对于许多忙碌的职场人士来说&#xff0c;时间和地点的限制使得他们无法像传统学生那样参加常规的大学教育。好在有着广西开放大学这样的学府&#xff0c;为那些繁忙而渴望知识更新的人们提供了学…