让天底下没有难学的链表2(回归超长加更学习爆爽篇)

目录

打印单链表

单链表的实现

1.尾插

2.头插

3.尾删

4.头删

5.查找

6.指定位置之前插入数据

 7.在指定位置之后插入数据

8.删除pos结点 

9.删除pos之后的结点

10.销毁链表

11.链表的复杂度的讨论


好了,那么上一篇文章我们大概了解了链表的性质以及基本结构

这里我们复习一下:

1.链表在逻辑结构上是连续的 , 在物理结构上不连续
2.结点一般是在堆上申请的
3.从堆上申请来的空间 , 是按照一定策略分配出来的 ,每次申请的空间可能连续 , 可能不连续。

那么这篇文章,我们就来具体探讨一下,如何手搓一个链表,以及链表的具体步骤有哪些。

打印单链表

首先

向内存中申请一个 结点大小 的空间

再是初始化结点数据(向结点中存储数据)

最后一步就是要把他们全部建立联系:

这里通过next指针把这四个节点串在一起。如果画成图的话就应该是这样:

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
 
void Test()
{
 
	//手动构造一个链表(结点)
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
 
	//初始化
	node1->data = 1;
	node2->data = 2;
	node3->data = 3;
	node4->data = 4;
 
	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;
 
}
 
int main()
{
	Test();
	return 0;
}

以上就是具体的代码实现,那么我们放到vs2022里面一运行,出来的结果就是这样:

那么接下去就是第二个部分。

单链表的实现

1.尾插

在插入数据之前 , 链表中可能没有数据(空链表) , 可能存了数据(非空链表)

这就是空链表和非空链表的区别,相应的,他们二者也有不同的尾插方法,但总体上一样,非空链表的话,因为我们说到他所在的物理意义上是不连续的,就像你的下级认识你,但是你的下下级可能就不会认识你一样,我的附庸的附庸不是我的附庸,这里就会牵扯到这样的一句经典的古话。所以需要有一个通信员去找到具体办事的人员

但是我们这里需要注意的点

1 . 无论是进行尾插,头插,还是在任意位置插入数据,都要 创建一个结点(向内存申请结点大小的空间) ,为了减少代码的重复书写 , 这里可以创建一个函数 STDBuyNode( )

//申请一个结点大小的空间
SLTNode* SLTBuyNode(SLTDateType x)
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
	if (node == NULL)
	{
		perror("malloc fail!");
		return 1;
	}
	node->data = x;
	node->next = NULL;
 
	return node;
}

那我们直接来看一下链表尾插的代码

//尾插
void SLTPushBack(SLTNode** pphead, SLTDateType x)
{
	SLTNode* newnode = SLTBuyNode(x);
	//链表为空
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	//链表不为空 --> 找尾结点
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next)//ptail->next != NULL
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

那我们一看这个代码,嗬,还整上二级指针了,这是为什么呢?我这里的传参为什么不能直接用plist去赋值呢?就像这样

但是事实上也就跟我在图中标注的一样,plist 是结构体指针变量 , 存储的是地址 , 如果想要进行地址的传递 , 需要用到二级指针!(二级指针的理解并不会很难 , 一级指针存放的是地址 , 同样的,二级指针存放的也是地址(一级指针的地址)).

所以我们就得牢记一句话,为了更突出,用图片的方式放上来:

那接下来我们就再来看一下二级指针和一级指针的关系,也像是一个链表一样

那么在这里,形参和实参对应的关系就很明确了。

2.头插

头插思路是什么呢?

1 . 先申请一块结点大小的空间 --> 新结点(newnode)

2 . 让 新结点 与 头结点 联系起来

3 . 让新结点成为   头结点

我们也说过,链表在物理意义上不连续,所以只需要让我们的新节点的指针域指向原来的头节点就可以了,也就是说换了个大领导,那原先的小领导还在,只要大领导跟小领导对接好工作,整条线还是能正常运行的。

//头插
void SLTPushFront(SLTNode** pphead, SLTDateType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

3.尾删

思路:

这里就出现了一个非常重要的细节:不能直接free 掉尾结点 ,因为尾结点的前一个结点的 next指针 依旧指向尾结点,直接free掉尾结点 , 会使next 指针变成野指针

1. 先遍历链表,找到尾结点

2. 存储尾结点前一个结点的next 值

3.让 next 的值 置为 NULL

4.free 掉 尾结点

注意 :当链表中只有一个结点的时候 , 进入不到循环中,此时的prev 依旧是NULL,对NULL解引用,程序会崩!

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* ptail = *pphead;
		SLTNode* prev = NULL;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		prev->next = NULL;
		free(ptail);
		ptail = NULL;
	}
}

也就是说,我得用prev先去储存这个节点的指针域让他为空,再释放尾节点,这样的程序才不会越界。

4.头删

思路;1 . 通过头结点的 next值 , 可以知道头结点的下一个结点的地址 , 使phead = phead->next , 就找不回头结点了 , 所以需要创建一个指针变量 , 存储头结点的下一个地址。

 2 . 然后free 掉 头结点

 3. 改变头结点的值

这里的问题和之前头删一样,不再过多叙述。

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

5.查找

通过循环while , 遍历数组 ,直到找到目标结点 , 就返回目标结点 , 没有目标结点,就返回NULL

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataTpye x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//没找到
	return NULL;
}

6.指定位置之前插入数据

思路:通过 创建一个指针变量 prev , 从头开始遍历链表 , 直到找到 pos 结点之前的结点 , 然后使newnode , 与prev 和 pos结点连接起来 。

注意:当pos == phead 时,起始prev->next 不等于 pos , 所以prev 会继续往后走,但始终都找不到pos , 程序会出现报错

//指定位置之前插入数据
SLTNode* SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataTpye x)
{
	assert(pphead && pos);
	if (pos == *pphead)
	{
		//相当于头插入
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* newnode = SLTBuyNode(x);
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

 7.在指定位置之后插入数据

思路:

1 . 在指定位置之后插入数据不需要 知道头指针

2 . 先将 newnode 与 pos 之后的结点建立起联系

3 . 再使 pos 结点 与 newnode 建立起联系

//指定位置之后插入数据
SLTNode* SLTInsertAfter(SLTNode* pos, SLTDataTpye x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

8.删除pos结点 

思路:1 . 创建一个指针变量存储 pos 结点的前一个结点(prev)

2 . 使 prev 与 pos的后一个结点建立联系

3 . free 掉 pos

4 . pos 置为NULL

注意 : 当 pos = phead 时候,prev 会一直往后走,所以对于这种情况需要特殊讨论(相当于头删)

9.删除pos之后的结点

思路:

1 . 为了避免pos 是尾结点 , 之后没有结点的这种情况  ,断言(pos->next)

2 . 创建一个指针变量 , 存储pos 结点的下一个结点(del)

3 . 让pos 与 del 的下一个结点建立起联系

4 . free 掉 del 

5 . del 置为 NULL

所以我们最后一步才需要一个del来储存真正的pos的下一个节点。

当然,如果pos后面为空,后面是没有数据的,我们要直接规避这种情况的发生。

10.销毁链表

思路:1 . 创建两个指针变量 , pcur (存储当前结点的地址) , next(存储下一个结点地址)

2 . 构建循环体(结束条件是pcur == NULL 时,结束循环) , 把结点一个一个的释放 , 先用next 把 pcur 下一个结点的地址存储起来 , 然后 free 掉 pcur , 再把 next 的值赋给 pcur

3 . 让*pphead 置为 NULL

我们创建链表是一个个创建的,释放的时候也得一个个的释放。

//销毁链表
SLTNode* SLTDestory(SLTNode** pphead)
{
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

11.链表的复杂度的讨论

 

我们再回顾一下,在讨论顺序表的时候我们通常所说的是什么

所以这也就呼应了我链表的第一篇文章所留下的疑问,链表到底强在哪?好像链表的名气确实是要比顺序表大很多。当然也不能一味的去说,链表一定比顺序表好 ,它们没有好坏之分 , 不同的场景可以运用不同的表 ,我们需要学习更多的数据结构 来解决不同的算法题。

如果你觉得对你有帮助,可以点赞关注加收藏,感谢您的阅读,我们下一篇文章再见。

一步步来,总会学会的,首先要懂思路,才能有东西写。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值