目录

探索单链表的奇妙世界

探索单链表的奇妙世界

[https://csdnimg.cn/release/blogv2/dist/pc/img/activeVector.png VibeCoding·九月创作之星挑战赛 10w+人浏览 1k人参与

https://csdnimg.cn/release/blogv2/dist/pc/img/arrowright-line-White.png]( )

前言:

通过学习顺序表,我们迈入了数据结构的大门。关于顺序表,值得深入思考以下几个问题:

(1)增容需要申请新空间,拷⻉数据,释放旧空间,会有不⼩的消耗,如何进行解决呢?

(2)增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间,又该如何解决呢?

(3) 中间/头部的插⼊删除,时间复杂度为O(N),又该如何进行优化呢?

为克服上述不足,我们引入了单链表这一新型数据结构。

如图所示:单链表头文件声明的函数,主要包含增、删、改、查四个功能。

https://i-blog.csdnimg.cn/direct/d384bfa290de4072873951bc4176e39c.png

https://i-blog.csdnimg.cn/direct/2b23d277da3b4cc683e9abd4f4379a97.gif

一、单链表的概念和结构

1.1单链表的概念

单链表(Single Linked List)是一种线性数据结构,由一系列节点(Node)组成,节点之间通过指针(或引用)连接,形成链式结构。与数组不同,链表的节点在内存中不需要连续存储,而是通过指针关联,因此具有更好的动态性。

1.2单链表的结构

1.逻辑结构:单链表采用线性逻辑结构,其组织方式类似于火车车厢的串联。每个节点(车厢)通过指针(编号)指向下一个节点,形成有序的线性序列。这种结构支持节点的动态增删,既节省了存储空间,又提高了空间利用率

如下图所示:

https://i-blog.csdnimg.cn/direct/10e59c87436e4a95a5a049002f7836b9.png

2.物理结构:单链表在物理结构上不同于数组是连续地址开辟的,单链表每个节点都是独立分配内存空间,通过单链表指针进行访问和使用。

1.3单链表的特点

核心特点:

1.动态内存分配:不需要预先分配固定大小的内存,节点可按需创建 / 释放,节省空间。

2.高效的插入 / 删除:在已知位置插入或删除节点时,只需修改指针指向(时间复杂度为O(1)),无需像数组那样移动大量元素。

二、单链表的实现

我们通过如下文件,实现单链表的增、删、改、查功能:

①SListNode.h       单链表函数的声明  及 单链表结构体的实现

②SlistNode.c        单链表函数的实现

③Test.c                  测试单链表的功能

2.1SListNode.h的声明


#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
		
//为单链表中存储的数据类型更名  以便后续数据类型的替换
typedef int SLTDataType;
		
//创建单链表结构体
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
	
//打印单链表
void SLTPrint(SLTNode* phead);
	

//申请一个新节点
SLTNode* SLTSetNode(SLTDataType);
	

//尾插一个节点
//这里需要一个二级指针,因为对头指针为空的情况而言,需要进行对头指针进行改变
//不能传值操作,而应该传址操作
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);
	

//指定节点后插入一个节点
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
	
	
//删除指定节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
		
	
//删除指定节点后的一个节点
void SLTEraseAfter(SLTNode* pos);
	
	
//销毁链表
void SLTDestroy(SLTNode** pphead);

对于这个头文件我们主要关注:

(1)单链表结构体的定义:


typedef int SLTDataType;
		

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

①首先定义一个结构体名为:struct SListNode

②将该结构体进行重命名为:typedef struct SListNode SLTNode

③将单链表存储的数据类型进行更名:typedef int SLTDataType;

④成员分析:

数据域:存储具体的数据(如整数、字符、对象等)

指针域:一个指向 “下一个节点” 的指针(或引用),用于连接相邻节点。

如下图所示: https://i-blog.csdnimg.cn/direct/cf0e3afa72d9465cb7fa4efd4ce76ad0.png

(2)重点明确以下指针关系:

①节点指针:用于存放该节点(单链表结构体)的地址

例如:节点1:0x0012FFB0

   ②每个节点中的指针:指向 “下一个节点” 的指针(或引用),用于连接相邻节点

例如:节点1中存放的:0x0012FFA0

2.2SListNode.c的实现

①单链表的打印


//打印节点信息
void SLTPrint(SLTNode* phead)
{
	while (phead)
	{
		printf("%d->", phead->data);
		phead = phead->next;
	}
	printf("NULL\n");
	
}

 以单链表中有以下节点为例,讲解代码运行

https://i-blog.csdnimg.cn/direct/d880d76e5b394301b039957bd42aa1c2.png

代码详解图:

https://i-blog.csdnimg.cn/direct/6b87789bd02a4929b730a479b562f2f6.png

核心代码:通过phead=phead->next,不断进行更新头节点phead的指向,达到while循环遍历整个单链表,使用phead为循环判断条件,当phead为空时,跳出循环,并打印NULL。

②节点的开辟


//开辟一个节点
SLTNode* SLTSetNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//判断是否成功开辟一个节点
	if (newnode == NULL)
	{
		perror("newnode");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
}

通过malloc动态函数在堆上为每个节点开辟空间,并存入值到节点的数据域中。

https://i-blog.csdnimg.cn/direct/a3a2125cf81f434c968864425d1200ee.png

温馨提示:刚申请的节点空间,由于不知道下一个节点的地址,所以将其置空。

③尾插节点(重点)


void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);  //头指针的地址不能为空,要对其进行解引用操作
	//创建一个节点
	SLTNode* newnode=SLTSetNode(x);
	
	//头节点为空(空链表)不能对ptail->next进行解引用
	//单独判断是否为空链表
	if (*pphead == NULL)
	{
		//直接将该头节点指向新节点
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;  //寻找尾节点
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//循环结束找到尾节点,将尾节点链接新节点
		ptail->next = newnode;
	}

}

 以单链表中有以下节点为例,讲解代码运行

情况一:当单链表不为空

https://i-blog.csdnimg.cn/direct/9e570f9b221c4e7da0d1f8110e38e318.png

情况二:当单链表为空

https://i-blog.csdnimg.cn/direct/e8629cbdde3d42b290a9aca65fcfde2e.png

代码解析:

(1)函数参数分析

void SLTPushBack(SLTNode pphead, SLTDataType x)**

①参数一SLTNode pphead   单链表结构体的二级指针**

有帅观众问这里为什么用单链表结构体的SLTNode 二级指针呢?**

用单链表结构体SLTNode*一级指针不就可以做到访问节点了。

因为头节点需要发生改变,如果用SLTNode ,则函数为void SLTPushBack(SLTNode pphead, SLTDataType x) ,那么这里会变成什么传递方式?**

头节点指针 作为实参,其类型为SLTNode ,而形参类型也为SLTNode,此时是值传递,值传递能改变头节点的信息吗?显然不行,所以要进行传递值操作,因而要用SLTNode** 这个二级指针作为形参类型。**

②参数二:SLTDataType x

核心作用:传入x,存入到节点的数据域中。

(2)详解如何进行尾插操作

核心思路:

①找到尾节点,将尾节点与新节点进行连接。

创建一个节点,用newnode进行维护,定义ptail指针赋值为头节点指针,通过while循环进行遍历,判断条件尾ptail->next!=NULL,将尾节点与新节点进行连接,即:ptail->next=newnode;

②考虑特殊情况,链表为空时。

当链表为空时,进行额外判断,将尾节点指针赋值给头节点指针即可,即*pphead=newnode;

④头插节点


//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);  //头节点的地址不能为空   (头指针可以为空)
	//创建一个节点
	SLTNode*newnode= SLTSetNode(x);

	//储存头节点
	newnode->next = *pphead;

	//更新头节点
	*pphead = newnode;
}

以单链表中有以下节点为例,讲解代码运行

情况一:当单链表含多节点

情况二:当节点为空时

https://i-blog.csdnimg.cn/direct/40844ffebd354eb688834f84e57fd646.png

代码解析:

核心思路:

①将头节点的指针存储到新申请的节点的指针域中

②更新头节点的指针存储位置

③考虑特殊情况,当头节点指针为空时,仍然满足上述代码情况,无需额外进行判断。

⑤尾删节点(重点)


//尾删
void SLTPopBack(SLTNode** pphead)
{
	//保证头节点的地址不为空,保证头节点不为空
	assert(pphead && *pphead);

	//找到尾节点和尾节点的前一个节点
	SLTNode* prev = *pphead;
	SLTNode* ptail = *pphead;

	//只有一个节点的情况
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个节点的情况
	else
	{
		//找到尾节点位置
		while (ptail->next)
		{
			//找到尾节点前一节点
			//保证了prev在ptail前一个节点处
			prev = ptail;
			ptail = ptail->next;
		}

		//释放尾节点
		free(ptail);
		ptail = NULL;

		//更新当前尾节点,如果是只有一个节点会出现问题
		prev->next = NULL;
	}

}

以单链表中有以下节点为例,讲解代码运行

情况一:当单链表含多节点

https://i-blog.csdnimg.cn/direct/f9c3c517dbdf4fc98706b196001772ff.png

情况二:当单链表只含有一个节点时

https://i-blog.csdnimg.cn/direct/fdfbfd482fac4719bd0e6abc362519df.png

代码解析:

核心思路:

*①assert(pphead && pphead);  保证头节点指针的地址不为空,要对其进行解引用,保证头节点指针不为空,确保单链表有一个节点可以删除

②核心步骤:定义一个ptail指针初始化为头节点指针,遍历链表找到尾节点,将尾节点进行删除,定义一个prev指针初始化为头节点指针,主要用于指向尾节点的前一个节点

③更新当前尾节点存储的指针域,将其置为NULL

④当只有一个节点时,ptail->next=NULL,此时寻找尾节点时对ptail->next解引用会出错,所以需要单独进行特殊判断。

⑥头删节点


//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);

	//保存当前头节点
	SLTNode* tmp = *pphead;

	//更新头节点
	*pphead= tmp->next;
		
	//释放原头节点
	free(tmp);

	//将原头节点置空
	tmp = NULL;
}

以单链表中有以下节点为例,讲解代码运行

情况一:包含多个节点

https://i-blog.csdnimg.cn/direct/348d310950324edb92a1252da41772e5.png

情况二:只有一个节点

https://i-blog.csdnimg.cn/direct/4132dbeb83b34b4893a8dca31c6cc7ac.png

代码解析:

核心思路:

*①assert(pphead && pphead);  保证头节点指针的地址不为空,要对其进行解引用,保证头节点指针不为空,确保单链表有一个节点可以删除

②临时保存头节点指针

③更新头节点指针,删除头节点

④考虑特殊情况,只有一个节点时,代码是否仍然适用。

思考:

这里会有帅观众问,为什么要临时保存头节点指针呢,直接将下一个节点的指针赋值给头节点指针,然后在free空间,不就做到了头删操作吗?


*pphead=(*pphead)->next;
free(*pphead);

        

这样操作可行吗?答案是肯定不行的,因为这样操作*pphead这个头节点指针已经发生了改变,我们还能找到原来的头节点吗?

正确操作是临时保存头节点指针,然后更新头节点指针,通过临时指针变量找到原头节点,将其free。

温馨提示:当只有一个节点时,仍然适用于上述代码。

⑦查找节点


//查找节点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	//若为空链表无法进行查询
	assert(phead);

	//若链表不为空
	//用临时指针pcur进行遍历每个节点
	SLTNode* pcur = phead;

	while (pcur)  //pcur!=NULL
	{
		if (pcur->data == x)
		{
			//printf("查找到了该元素!\n");
			return pcur;
		}
		pcur = pcur->next;
	}

	//遍历结束,未找到
	//printf("未查找到该元素 !\n");
	return NULL;
}

以单链表中有以下节点为例,讲解代码运行

情况一:包含多个节点

https://i-blog.csdnimg.cn/direct/3dba8fb72250400fa5c4c25f68f2bf28.png

情况二:只含有一个节点

https://i-blog.csdnimg.cn/direct/4132dbeb83b34b4893a8dca31c6cc7ac.png

代码解析

核心思路:

①遍历单链表中的每个节点,如果有节点的数据域与查找的数据相同,进行返回该节点

②遍历单链表时,我们通常会用临时变量pcur保存头节点指针,防止形参中头节点指针发生改变,后续如果要再次进行遍历时,会导致找不到头节点指针的位置。

⑧指定节点前插入节点(重点)


//指定节点前插入一个数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);  //头节点的地址不能为空,头节点不能为空
	
	//开辟一个节点进行插入。
	SLTNode *newnode=SLTSetNode(x);
	
	//如果pos为头节点
	if ((*pphead)==pos)
	{
		//进行头插
		SLTPushFront(pphead,x);
	}
	else
	{
		//用prev去保存头节点
		SLTNode* prev = *pphead;   
		//寻找pos前一个节点位置
		while (prev->next != pos)
		{
			prev=prev->next;
		}

		//找到了pos前一个节点的位置,更新newnode存储的下一个节点信息
		newnode->next = pos;

		//更新pos前一个节点的位置
		prev->next = newnode;

	}

}

情况一:在中间进行插入一个节点

https://i-blog.csdnimg.cn/direct/a059f8378f104188ac36dd3aaf8c0853.png

情况二:在尾部插入一个节点

https://i-blog.csdnimg.cn/direct/32d1ccc15ff54f0fb4efaa98efbb152b.png

情况三:在头部进行插入一个节点

https://i-blog.csdnimg.cn/direct/fb4c88df62044a30b7bac53d9cbda48f.png

代码解析

核心思路:

①若链表为空时,不能进行指定位置前进行插入一个节点,因为此时pos位置为NULL,无法对其进行解引用,如图所示https://i-blog.csdnimg.cn/direct/2a2292cf778c49368dba0d32d0206fd4.png

因此单链表至少要有一个头节点时才能进行在指定位置前进行插入一个节点。

②如何在pos节点前进行插入一个节点呢?

(1)申请一个节点

申请一个节点,用节点指针newnode进行维护

(2)要找到pos节点位置前的一个节点。

通过定义一个prev指针保存头节点指针,利用while循环,判断条件为prev->next!=pos,进行寻找到pos节点位置前的一个节点。

(3)修改pos节点位置前的一个节点的指针域,将其修改为新节点的地址。

注:将newnode的指针域指向 pos节点的地址,再将newnode节点的地址存储到pos节点位置前的一个节点指针域中。

即newnode->next = pos;    prev->next = newnode;

(4)考虑特殊情况只有一个节点

注:当只有一个节点时,在其前面插入一个节点,这就相当于进行头插操作,调用头插函数即可,将代码进行复用

⑨指定节点后插入节点


//在指定节点后插入一个节点
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	//链表一定要存在才能进行后插
	assert(pos  && "插入位置无效");

	//开辟一个新节点
	SLTNode * newnode=SLTSetNode(x);

	newnode->next = pos->next;

	pos->next = newnode;

}

以单链表中有以下节点为例,讲解代码运行

https://i-blog.csdnimg.cn/direct/96ed870df90e4f4da739b792a32cb244.png

代码详解

核心思路:

①链表不能为空,如果链表为空,pos节点位置将无效,要进行断言判定。

②将新申请的节点的指针域更改为pos节点存储的指针域信息

③将pos节点存储的指针域信息更改为新申请的节点指针

⑩删除指定节点(重点)


//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	//头节点不能为空,保证链表至少有一个节点可以进行删除
	//头节点的地址不能为空否则不能进行解引用操作
	assert(pphead&&*pphead);  
	assert(pos && "插入位置无效");

	//当pos处为节点时
	if (*pphead == pos)
	{
		//想当于头删
		SLTPopFront(pphead);
	}
	else
	{
		//查找pos节点前一个节点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//更新prev的下一个节点信息
		prev->next = pos->next;   
		free(pos);
		pos = NULL;
	}

}

以单链表中有以下节点为例,讲解代码运行

情况一:pos位置不在头节点处

https://i-blog.csdnimg.cn/direct/63d73b37107e43a7930abf3af4158e3a.png

情况二:pos位置在头节点处

https://i-blog.csdnimg.cn/direct/1fb5f233c3884d968d2d165ea4e798ee.png

代码解析

核心思路:

       

①要提前判断单链表是否为空,如果是空链表则不能进行删除

*通过assert进行断言,assert(pphead&&pphead);    assert(pos && “插入位置无效”);

②要找到pos节点前的一个节点位置,并且修改pos前一个节点的指针域,将其修改为pos节点后的一个节点的地址。

定义一个prev节点指针,初始化为头节点地址,通过while循环进行遍历,循环条件为prev->next!=pos  退出循环则****为pos节点前的一个节点,将其进行修改为prev->next = pos->next;

③最后释放pos节点的空间

free(pos);

④特别注意:当pos节点为头节点时,寻找pos节点前的一个节点位置可能出现问题,要进行额外判断

如情况二所示,当pos节点为头节点时,prev->next为NULL不等于pos,会无限进入循环,导致出错,所以这里要进行额外判断,当pos为头节点时,此时相当于头删操作,调用头删函数即可。

⑪删除指定节点后一个节点


//删除指定位置后一个节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);   //保证至少有两个节点

	//临时保存pos节点处的下一个节点
	SLTNode* tmp = pos->next;

	//更新pos节点处的下一个节点信息
	pos->next = tmp->next;

	//释放pos节点处的下一个节点
	free(tmp);
	tmp = NULL;
}

以单链表有如下节点为例,讲解代码运行:

https://i-blog.csdnimg.cn/direct/9399c93ecc8a417c8407d9d2f279215d.png

代码解释

核心思路:

① 单链表不能为空,且至少得包含两个节点

② 如何进行删除指定位置后的一个节点呢?

(1) 通过创建临时节点指针,保存pos节点位置后的节点

(2) 更新pos节点的指针域

(3) 释放pos节点后的节点空间

思考:

如果不进行临时储存pos节点后的一个节点直接进行更新pos节点的指针域会出现什么情况?

则可以写出如下代码:

pos->next=pos->next->next;

free(pos->next);

温馨提示:此时还能正确删除吗?答案显然不能,因为先改变了pos的指针域,导致pos->next已经改变,无法正确找到pos节点后的一个节点

⑫销毁链表(重点)


void SLTDestroy(SLTNode** pphead)
{
	assert(*pphead && pphead);  //保证头节点的地址和头节点都不为空

	//定义pcur 保存头节点 进行遍历链表
	SLTNode* pcur = *pphead;

	while (pcur)
	{
		//保存pcur的下一个节点信息
		SLTNode* pnext = pcur->next;
        
		//释放当前节点的空间
		free(pcur);

		//更新当前pcur指向的节点
		pcur = pnext;
	}

	//头节点空间释放,但头节点仍然指向那块空间,防止野指针进行置空处理
	*pphead = NULL;
}

以如下单链表讲解:

https://i-blog.csdnimg.cn/direct/6cd971c3ee0e4d19b8b41b7055084515.png

代码详解

核心思路:

①作为习惯,为了不改变形参中头节点的位置,通过创建临时变量pcur进行遍历单链表     

        

②通过循环进行遍历,依据循环条件pcur!=NULL;

③通过临时变量pnext,保存pcur的下一个节点信息

④释放当前pcur所指向的节点

思考:如果不进行零时变量存储pcur的下一个节点信息,直接通过pcur=pcur->next; 进行更新pcur指向的节点

这样pcur是先移动指针,这样就会导致释放的是已经移动后的节点,而非当前需要释放的节点。

三、单链表的源码

3.1SListNode.h


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

typedef int SLTDataType;
		

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;
	

void SLTPrint(SLTNode* phead);
	

//申请一个新节点
SLTNode* SLTSetNode(SLTDataType);
	

//尾插一个节点
//这里需要一个二级指针,因为对头指针为空的情况而言,需要进行对头指针进行改变
//不能传值操作,而应该传址操作
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);
	

//指定节点后插入一个节点
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
	
	
//删除指定节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
		
	
//删除指定节点后的一个节点
void SLTEraseAfter(SLTNode* pos);
	
	
//销毁链表
void SLTDestroy(SLTNode** pphead);

3.2SListNode.c


#include"SListNode.h"


//打印节点信息
void SLTPrint(SLTNode* phead)
{
	while (phead)
	{
		printf("%d->", phead->data);
		phead = phead->next;
	}
	printf("NULL\n");
	
}


//开辟一个节点
SLTNode* SLTSetNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	//判断是否成功开辟一个节点
	if (newnode == NULL)
	{
		perror("newnode");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
}


//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);  //头指针的地址不能为空
	//创建一个节点
	SLTNode* newnode=SLTSetNode(x);
	
	//头节点为空(空链表)不能对ptail->next进行解引用
	//单独判断是否为空链表
	if (*pphead == NULL)
	{
		//直接将该头节点指向新节点
		*pphead = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;  //寻找尾节点
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		//循环结束找到尾节点,将尾节点链接新节点
		ptail->next = newnode;
	}

}


//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);  //头节点的地址不能为空   (头指针可以为空)
	//创建一个节点
	SLTNode*newnode= SLTSetNode(x);

	//储存头节点
	newnode->next = *pphead;

	//更新头节点
	*pphead = newnode;
}


//尾删
void SLTPopBack(SLTNode** pphead)
{
	//保证头节点的地址不为空,保证头节点不为空
	assert(pphead && *pphead);

	//找到尾节点和尾节点的前一个节点
	SLTNode* prev = *pphead;
	SLTNode* ptail = *pphead;

	//只有一个节点的情况
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//多个节点的情况
	else
	{
		//找到尾节点位置
		while (ptail->next)
		{
			//找到尾节点前一节点

			//保证了prev在ptail前一个节点处
			prev = ptail;
			ptail = ptail->next;
		}

		//释放尾节点
		free(ptail);
		ptail = NULL;

		//更新当前尾节点,如果是只有一个节点会出现问题
		prev->next = NULL;
	}

}


//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);

	//保存当前头节点
	SLTNode* tmp = *pphead;

	//更新头节点
	*pphead= tmp->next;
		
	//释放原头节点
	free(tmp);

	//将原头节点置空
	tmp = NULL;
}


//查找节点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	//若为空链表无法进行查询
	assert(phead);

	//若链表不为空
	//用临时指针pcur进行遍历每个节点
	SLTNode* pcur = phead;

	while (pcur)  //pcur!=NULL
	{
		if (pcur->data == x)
		{
			//printf("查找到了该元素!\n");
			return pcur;
		}
		pcur = pcur->next;
	}

	//遍历结束,未找到
	//printf("未查找到该元素 !\n");
	return NULL;
}


//指定节点前插入一个数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);  //头节点的地址不能为空,头节点不能为空
	
	//开辟一个节点进行插入。
	SLTNode *newnode=SLTSetNode(x);
	
	//如果pos为头节点
	if ((*pphead)==pos)
	{
		//进行头插
		SLTPushFront(pphead,x);
	}
	else
	{
		//用prev去保存头节点
		SLTNode* prev = *pphead;   
		//寻找pos前一个节点位置
		while (prev->next != pos)
		{
			prev=prev->next;
		}

		//找到了pos前一个节点的位置,更新newnode存储的下一个节点信息
		newnode->next = pos;

		//更新pos前一个节点的位置
		prev->next = newnode;

	}

}


//在指定节点后插入一个节点
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	//链表一定要存在才能进行后插
	assert(pos  && "插入位置无效");

	//开辟一个新节点
	SLTNode * newnode=SLTSetNode(x);

	newnode->next = pos->next;

	pos->next = newnode;

}


//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	//头节点不能为空,保证链表至少有一个节点可以进行删除
	//头节点的地址不能为空否则不能进行解引用操作
	assert(pphead&&*pphead);  
	assert(pos && "插入位置无效");

	//当pos处为节点时
	if (*pphead == pos)
	{
		//想当于头删
		SLTPopFront(pphead);
	}
	else
	{
		//查找pos节点前一个节点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//更新prev的下一个节点信息
		prev->next = pos->next;   
		free(pos);
		pos = NULL;
	}

}

//删除指定位置后一个节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);   //保证至少有两个节点

	//临时保存pos节点处的下一个节点
	SLTNode* tmp = pos->next;

	//更新pos节点处的下一个节点信息
	pos->next = tmp->next;

	//释放pos节点处的下一个节点
	free(tmp);
	tmp = NULL;
}

void SLTDestroy(SLTNode** pphead)
{
	assert(*pphead && pphead);  //保证头节点的地址和头节点都不为空

	//定义pcur 保存头节点 进行遍历链表
	SLTNode* pcur = *pphead;

	while (pcur)
	{
		//保存pcur的下一个节点信息
		SLTNode* next = pcur->next;

		//释放当前节点的空间
		free(pcur);

		//更新当前pcur指向的节点
		pcur = next;
	}

	//头节点空间释放,但头节点仍然指向那块空间,防止野指针进行置空处理
	*pphead = NULL;
}

四、单链表展示

4.1尾插节点


	//创建一个空链表
	SLTNode* node1 = NULL;

	//尾插四个元素
	SLTPushBack(&node1, 1);
	SLTPushBack(&node1, 2);
	SLTPushBack(&node1, 3);
	SLTPushBack(&node1, 4);
		
	//打印链表
	SLTPrint(node1);

4.2尾删节点


//创建一个空链表
SLTNode* node1 = NULL;

//尾插四个元素
SLTPushBack(&node1, 1);
SLTPushBack(&node1, 2);
SLTPushBack(&node1, 3);
SLTPushBack(&node1, 4);

printf("单链表尾插节点后:\n");
SLTPrint(node1);

SLTPopBack(&node1);
SLTPopBack(&node1);
SLTPopBack(&node1);
SLTPopBack(&node1);

printf("单链表尾删节点后:\n");
SLTPrint(node1);

https://i-blog.csdnimg.cn/direct/c67f1edff6c84e54984913910cbf8612.png

4.3头插节点


	//创建一个空链表
	SLTNode* node1 = NULL;

	printf("当前单链表情况:\n");
	SLTPrint(node1);

	SLTPushFront(&node1, 99);
	SLTPushFront(&node1, 88);
	SLTPushFront(&node1, 77);
	SLTPushFront(&node1, 66);
	printf("单链表前插节点后:\n");
	SLTPrint(node1);

4.4头删节点


//创建一个空链表
SLTNode* node1 = NULL;

printf("当前单链表情况:\n");
SLTPrint(node1);

//前插四个节点
SLTPushFront(&node1, 99);
SLTPushFront(&node1, 88);
SLTPushFront(&node1, 77);
SLTPushFront(&node1, 66);

printf("单链表前插节点后:\n");
SLTPrint(node1);

//头删四个节点
SLTPopFront(&node1);
SLTPopFront(&node1);
SLTPopFront(&node1);
SLTPopFront(&node1);

printf("单链表头删节点后:\n");
SLTPrint(node1);

4.5查找节点


	//创建一个空链表
	SLTNode* node1 = NULL;

	printf("当前单链表情况:\n");
	SLTPrint(node1);

	//前插四个节点
	SLTPushFront(&node1, 99);
	SLTPushFront(&node1, 88);
	SLTPushFront(&node1, 77);
	SLTPushFront(&node1, 66);

	printf("单链表前插节点后:\n");
	SLTPrint(node1);
	
	SLTNode* find=SLTFind(node1, 99);
	printf("查找的当前节点的数据为:\n%d", find->data);

https://i-blog.csdnimg.cn/direct/950f0ab3873141f7ba9f7e629590f88f.png

4.6指定节点前插入一个节点


	//创建一个空链表
	SLTNode* node1 = NULL;

	printf("当前单链表情况:\n");
	SLTPrint(node1);

	//前插四个节点
	SLTPushFront(&node1, 4);
	SLTPushFront(&node1, 3);
	SLTPushFront(&node1, 2);
	SLTPushFront(&node1, 1);

	printf("单链表前插节点后:\n");
	SLTPrint(node1);
	
	SLTNode* find=SLTFind(node1, 2);

	//在节点2之前插入一个节点  插入的值为99
	SLTInsert(&node1, find, 99);
	
	//打印插入节点后的信息
	printf("在节点2之前插入一个节点:\n");
	SLTPrint(node1);

4.7指定节点后插入节点


	//创建一个空链表
	SLTNode* node1 = NULL;

	printf("当前单链表情况:\n");
	SLTPrint(node1);

	//前插四个节点
	SLTPushFront(&node1, 4);
	SLTPushFront(&node1, 3);
	SLTPushFront(&node1, 2);
	SLTPushFront(&node1, 1);

	printf("单链表前插节点后:\n");
	SLTPrint(node1);
	
	SLTNode* find=SLTFind(node1, 2);

	//在节点2之后插入一个节点  插入的值为99
	SLTInsertAfter(find, 99);
	
	//打印插入节点后的信息
	printf("在节点2之后插入一个节点:\n");
	SLTPrint(node1);

https://i-blog.csdnimg.cn/direct/65918019ab7446e189a7ab26a8389bdf.png

4.8删除指定节点


	//创建一个空链表
	SLTNode* node1 = NULL;

	printf("当前单链表情况:\n");
	SLTPrint(node1);

	//前插四个节点
	SLTPushFront(&node1, 4);
	SLTPushFront(&node1, 3);
	SLTPushFront(&node1, 2);
	SLTPushFront(&node1, 1);

	printf("单链表前插节点后:\n");
	SLTPrint(node1);
	
	SLTNode* find=SLTFind(node1, 2);

	//删除节点2
	SLTErase(&node1, find);
	printf("单链表删除2后:\n");
	SLTPrint(node1);

https://i-blog.csdnimg.cn/direct/b87b57469cd046d1a96110fdb31b0053.png

4.9删除指定节点后一个节点


//创建一个空链表
SLTNode* node1 = NULL;

printf("当前单链表情况:\n");
SLTPrint(node1);

//前插四个节点
SLTPushFront(&node1, 4);
SLTPushFront(&node1, 3);
SLTPushFront(&node1, 2);
SLTPushFront(&node1, 1);

printf("单链表前插节点后:\n");
SLTPrint(node1);

SLTNode* find=SLTFind(node1, 2);

//删除节点2后的一个节点
SLTEraseAfter(find);
printf("单链表删除2后的一个节点:\n");
SLTPrint(node1);

4.10销毁链表


	//创建一个空链表
	SLTNode* node1 = NULL;

	printf("当前单链表情况:\n");
	SLTPrint(node1);

	//前插四个节点
	SLTPushFront(&node1, 4);
	SLTPushFront(&node1, 3);
	SLTPushFront(&node1, 2);
	SLTPushFront(&node1, 1);

	printf("销毁链表前:\n");
	SLTPrint(node1);
	
	printf("销毁链表后:\n");
	SLTDestroy(&node1);
	SLTPrint(node1);

https://i-blog.csdnimg.cn/direct/f9f4d3e5a93542ccba9488316a4ca1de.png

五、总结反思

(1)重点一:

明确传参时何时要进行对头节点进行传地址操作,当要修改头节点时,形参要改变实参,就需要进行传址传递

(2)重点二:

要特别注意空链表的判断

(3)重点三:

对头节点的处理和尾节点的处理,以及对空链表的处理要额外进行关注。

既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

https://i-blog.csdnimg.cn/direct/6ef2ad1be2754cc1af0b1e2c24548b79.gif