【数据结构】单链表专题(详细代码及配图)

小编主页详情<-请点击
小编gitee代码仓库<-请点击


本文主要介绍了数据结构的单链表,内容全由作者原创(无AI),同时深度解析了单链表增删查改等功能,并带有配图帮助博友们更好的理解,点个关注不迷路,下面进入正文~~


前言:

学习了顺序表之后,我们发现顺序表存在以下问题:
1.中间/头部插入效率低下
2.增容降低运行效率
3.增容造成空间浪费

那么有什么办法可以解决这些问题?下面我们来介绍链表的概念

1.链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的顺序结构是通过链表中的指针链接次序实现的。

链表也是线性表的一种~
物理结构:不是线性的
逻辑结构:一定是线性的

那么链表具体是什么东西呢?

举一个具体的例子:

链表的结构跟火车很类似,如果是旺季,我们可以增加车厢;如果是淡季,我们可以减少车厢,每一节车厢都是独立的,添加或减少车厢不会影响其他车厢。

想象一下,假如每节车厢的车门都是上锁的,并且需要用不同的钥匙打开,每次只能携带一把钥匙的情况下如何从车头走到车尾?

最简单的做法,在每一节车厢都存放下一届节车厢的钥匙。

在链表里,每一节车厢是长什么样的?

每个车厢,我们称之为节点/结点

节点的组成主要有两个部分:当前节点要保存的数据 和保存下一个节点的地址 (指针变量)。

图中的指针变量plist保存的是第一个节点的地址,我们称plist此时指向第一个节点。如果我们希望plist指向第二个节点时,只需修改plist保存的内容为0x0012FFA0。

2.单链表的具体功能的实现

2.1单链表需要实现的功能

cs 复制代码
//打印链表
void SLTPrint(SLTNode* phead);

//尾插
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);

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);

//销毁链表
void SListDesTroy(SLTNode** pphead);

2.2打印链表

通过phead(第一个节点的指针)先访问数据,再移动到head->next(下一个节点的指针),知道phead为空才停止访问,最后打印NULL收尾。

我们习惯用临时变量pcur保存第一个节点的地址,这样做是为了方便我们后续找到第一个节点的地址。

cs 复制代码
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)//pcur != NULL
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

2.3链表的的尾插

在实现尾插之前,我们先画个图帮助我们理解
思路:
1.创建新节点

为了方便后续创建新节点的使用,我们将创建新节点这一步骤定义为一个函数SLTBuyNode

cs 复制代码
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

2.创建尾节点的指针

我们要尾插一个节点,因此我们要找到尾节点的指针ptail,然后将ptail->next改为新创建节点的地址,因此找到尾节点是必要的。

我们可以先将ptail指向第一个节点,当ptail->nex不等于NULL时将ptail = ptail->next,直到ptail->next为NULL,此时就说明了我们找到尾节点的指针了。

cs 复制代码
SLTNode* ptail = *pphead;
while (ptail->next)
{
	ptail = ptail->next;
}

3.将ptail->next指向新节点

cs 复制代码
ptail->next = newnode

那我们不加思考地写出完整的代码

cs 复制代码
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	SLTNode* newnode = SLTBuyNode(x);
	SLTNode* ptail = phead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;
}

我们发现程序并没有像我们如期的一样打印1->NULL,这是为什么 呢?

我们想想,如果我们的链表一开始为空链表,及plist=phead=ptail=NULL,那我们还能进行ptail->next吗?很显然不行,程序会报错。因此我们需要判断传入的phead是否为NULL,如果是,直接让phead指向新节点;如果不是,再进行后面的操作,新代码如下

cs 复制代码
void SLTPushBack(SLTNode* phead, SLTDataType x)
{
	//assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		SLTNode* ptail = phead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

我们再运行一下

我们发现这次程序成功输出了,但却只输出了NULL,并没有输出我们预想的1->NULL,这又是为什么呢?别急,我们调试一下

我们发现plist并没有指向新的节点,而是NULL,说明plist的值并没有发生改变

这是因为我们在函数传参的时候使用的是传值调用,形参的改变要影响实参,必须要传地址

第一个节点 *plist<------>**pphead

指向第一个节点的指针 plist<------->*pphead

指向第一个节点的指针的地址 &plist<------>pphead

那么我们要改变指向第一个节点的指针,就要传入指向第一个节点的指针的地址 &plist

同时不要断言pphead,assert(pphead),防止传入了一个空指针NULL,下面是修改后正确的代码

cs 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//*pphead 就是指向第一个节点的指针
	//空链表和非空链表
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找尾
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

我们运行一下代码

结果是1->NULL,结果没有任何问题

通过这个头插的代码,给了我们两个启发:

1.写代码的时候,要着重思考头和尾的特殊情况
2.当我们需要改变函数的实参,必须要传入实参的地址

2.4链表的头插

思路:
1.创建新节点
2.newnode->next=*pphead
3.*pphead=newnode

cs 复制代码
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

我们检查一下,当链表为空链表时,*pphead为NULL,newnode->next为NULL,*pphead被赋值为新创建节点的地址,没有任何问题

2.5链表的尾删

思路:
1.创建ptail找尾节点的地址
2.创建prev找到尾节点前一个节点的地址
3.将ptail指向的节点的空间释放掉,并置为NULL
4.prev->next=NULL;
5.断言*pphead和pphead,如果链表为空那就没有节点可以删除

根据这个思路,我们写出如下代码:

cs 复制代码
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead&&*pphead);
	SLTNode* ptail = *pphead;
	SLTNode* prev = *pphead;
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	free(ptail);
	ptail = NULL;
	prev->next = NULL;
}

同样的,我们来思考一下,如果链表只有一个节点,代码可不可行?

ptail和prev都可以正常创建,ptail->next=NULL,不会进入循环,ptail指向的空间被释放掉,注意,ptail和prev指向的是同一块空间,因此prev此时是野指针,对野指针使用箭头操作符访问成员元素,编译器会直接报错,因此,如果链表只有一个节点,代码不可行

当只有一个节点时,不用创建prev和ptail变量,只需将*pphead指向的空间释放掉并置为NULL即可,更正后的代码如下:

cs 复制代码
void SLTPopBack(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead&&*pphead);
	//链表只有一个节点
	if ((*pphead)->next == NULL)//->优先级高于*
	{
		free(*pphead);
		*pphead = NULL;
	}
	//链表有多个节点
	else
	{
		SLTNode* ptail = *pphead;
		SLTNode* prev = *pphead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

这里需要注意,->优先级高于*,因此需要用( )先对pphead进行解引用操作

2.6链表的头删

思路:
1.链表不能为空
2.创建next变量存储(*pphead)->next
3.free(*pphead)
4.*pphead=next

cs 复制代码
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;//-> 优先级高于*
	free(*pphead);
	*pphead = next;
}

2.7链表的查找

如果找到了数据为x的节点,返回此节点的地址;如果没找到,返回空指针NULL

cs 复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
			return pcur;
		pcur = pcur->next;
	}
	return NULL;
}

2.8在指定位置之前插入数据

思路:
1.创建新节点
2.找到pos的前一个节点prev
3.prev->next=newnode
4.newnode->next=pos

根据思路我们写出如下代码:

cs 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	SLTNode* newnode = SLTBuyNode(x);
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = newnode;
	newnode->next = pos;
}

那么,当链表只有一个节点的时候,还能正常头插吗?

当链表只有一个节点的时候,*pphead=pos=prev,prev->next为NULL,prev->next!=pos条件为真,进入循环,prev被赋值为NULL。对为NULL的prev进行箭头访问数据时,编译器会报错,不能正常头插。

因此当*pphead=pos时,直接使用链表头插函数,正确的代码如下

cs 复制代码
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	//若pos == *pphead;说明是头插
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = SLTBuyNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

2.9在指定位置之后插入数据

问一个问题:先红色后绿色可行吗?

答案:不可行!

当pos->next=newnode时,就无法通过pos->nest找到下一个节点了

正确代码如下

cs 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

2.10删除pos节点

思路:
1.创建变量prev指向pos前一个节点
2.prev->next=pos->next
3.释放pos并置NULL

根据这个思路我们写出下面代码

cs 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

如果链表中pos为头节点,还能正常删除吗?我们推测一下

如果pos为头节点,那么刚开始的时候*pphead=pos=prev,prev->next为NULL,prev->next!=pos条件为真,进入循环,prev被赋值为NULL。对为NULL的prev进行箭头访问数据时,编译器会报错,不能正常删除。

因此当*pphead=pos时,直接使用链表头删函数,正确的代码如下

cs 复制代码
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

2.11删除pos之后的节点

思路:
1.如果先将pos->next指向pos->next->next,那么将无法找到需要删除的数据并无法将其释放

如果先将pos->next释放掉,那么将无法通过pos->next找到pos->next->next

因此我们要先定义一个临时变量del记录pos->next
2.将pos->next指向pos->next->next
3.释放del并置零

需要注意pos不能为最后一个节点,所以需要对pos->next断言,下面为正确的代码

cs 复制代码
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

2.12销毁链表

**思路:**通过pcur遍历整个链表,用next记录pcur->nest即pcur的下一个节点的地址,将pcur所指向的空间释放掉后,再将pcur赋值为next。当pcur为NULL时停止循环。正确代码如下:

cs 复制代码
void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	SLTNode* next = *pphead;
	while (pcur)
	{
		next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

结语:

这篇文章全文由作者手写,图片由画图软件所制,无AI制作,希望各位博友能有所收获

,欢迎各位博友的讨论,觉得不错的小伙伴,别忘了点赞关注哦~

相关推荐
j_xxx404_2 小时前
蓝桥杯--搜索(DFS基础回溯法,剪枝,记忆化搜索)
数据结构·c++·算法·蓝桥杯·深度优先·图论
王璐WL2 小时前
【C++】vector 在OJ中的使用
开发语言·c++
AI-小柒2 小时前
大模型API中转推荐:Dataeyes API 600+模型统一网关与负载均衡部署,claude编程、香蕉生图、视频大模型聚合平台
大数据·运维·开发语言·人工智能·算法·机器学习·负载均衡
逆境不可逃2 小时前
【用AI学Agent】ReAct框架(实现自主闭环,搞定复杂任务)
人工智能·算法·机器学习·职场和发展
牧瀬クリスだ2 小时前
二叉树的基本操作
数据结构·算法
白毛大侠2 小时前
内存对齐算法:向上取整到位运算
算法
Book思议-2 小时前
【数据结构】线索二叉树之中序遍历线索化详解与实现
数据结构·算法·线索二叉树之中序遍历线索化
knighthood20012 小时前
VTK/PCL点云可视化:解决加载后需要手动缩放的问题
c++·pcl
2501_920627612 小时前
Flutter 框架跨平台鸿蒙开发 - 算法可视化应用
算法·flutter·华为·harmonyos