单链表的实现


前言

我们这一节呢主要是讲一些单链表的相关知识,其中包括了单链表的概念以及相关的插入和删除
在这里插入图片描述


一、链表的概念及结构

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

结构:链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。那么车厢是什么样子的呢?
在这里插入图片描述
与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点”。节点的组成主要有两个部分: 当前节点要保存的数据和保存下⼀个节点的地址(指针变量)图中指针变量 plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0。

为什么还需要指针变量来保存下⼀个节点的位置?
链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。
因为每个节点都是独立存在的,那我们可以写出每个 结构体代码:

typedef int SLDataTyep;
typedef struct SListNode
{
	SLDataTyep data;//数据节点
	struct  SListNode* next;//指向下一个节点的指针	
}SLTNode;

补充说明:
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续
2、节点⼀般是从堆上申请的
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

二、单链表的插入

单链表的插入呢一共有:尾插,头插,在指定位置之前插入数据,在指定位置之后插入数据四部分。接下我们一个个的介绍。

1.尾插:什么是尾插呢?

我们通过画图可以直观的了解:

在这里插入图片描述

如图所示,我们要想在4这个节点之后插入一个新的节点newnode,这个时候第一个NULL 不是指向为空了,而是指向新节点newnode,这时新节点的指向下一个节点的指针却指向NULL了。那我们该如何在最后一个节点后面插入一个新的节点呢?首先我们先要找到最后一个节点,我们怎样找到最后一个节点呢?这里我们可以用一个循环来遍历一遍就可以找到了:先看代码:

void SLPushBank(SLTNode** pphead, SLDataTyep x)
{
	assert(pphead);
	//申请空间
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	//尾插
	if (*pphead == NULL)
	{
		*pphead = newnode;//链表为空的情况
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next )
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

这里我们可以看见申请空间哪里。我们细想一下,是不是只要是插入一个新的节点我们就要重新申请空间呀。那这里我们能不能把申请空间的代码写成一串函数,到时候要用的时候直接调用就OK了呀?

SLTNode* SListBuyNode( SLDataTyep x)//申请空间
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

像这样的话我们要用的时候就直接调用就行了。

所尾插的代码如下:

void SLPushBank(SLTNode** pphead, SLDataTyep x)//尾插
{
	assert(pphead);
	SLTNode* newnode = SListBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;//链表为空的情况
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next )
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

那我们这里为什么要用到一个二级指针指针呢?这里就要用到“传址调用”了。因为我们这里要传入头节点指针的地址,如果只是传头节点的指针的话,那么这里只是改变了指针的指向,然而不会影响实参的变化,那么怎样才能使他改变实参呢?这里就需要传入头节点指针的地址了,因为传入的时头节点指针的地址,那我们就需要一个二级指针来接收。总之要想形参影响实参,那就要传地址
在这里插入图片描述

2.头插

在这里插入图片描述
我们在来看看代码的实现:

void SListPushFront(SLTNode** pphead, SLDataTyep x)
{
	assert(pphead);
	SLTNode* newnode = SListBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

头插就是这样子的,其实也很简单的。那我们俩看看比较有趣的——在指定的位置之前插入新的节点

3.在指定位置之前插入数据

我们先来画图:
在这里插入图片描述
这里我们可以看出在pos之前插入数据。其实就两步就行了。第一步:先申请一个新的节点。第二步:使数据3指向4的指针指向下,而x指向下一个节点的指针指向4就行了。这样就刚好首尾链接在一起了。这里有个比较难的点就是该如何找到pos之前的数据。我们先来看看代码的实现:

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataTyep x)//在指定位置之前插入数据
{
	assert(*pphead && pphead);
	assert(pos);
	SLTNode* newnode = SListBuyNode(x);
	if (*pphead == pos)
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}

这里我们可以看到这个函数一共引入了三个参数。**pphead是头节点。pos是在pos之前插入数据。x是要插入的数据。 通过这个代码的实现呢,我们可以看见有两种情况,第一种情况呢则是考虑到空链表的情况,第二种则是不是空链表的情况。那我们这里要怎样才能找到pos之前的节点呢?这里我们也可以用一个循环来解决。只是需要注意一下循环的条件。我们在写代码的时候要考虑到极端的情况。还有就是写完之后一定要运行一下。


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

在这里插入图片描述
从这张图中我们可以思考一下,在指定的数据之后插入数据时,我们这时该用第二种代码还是第二种呢?
其实呀。这里用第二种代码是没有问题的。然而第一种的代码是有问题的。第一种代码呢实现了newnode指向它自己了。那如何解决这个问题呢?其实也很简单,只需要在给一个变量储存一下pos->next;就行了
来看看代码实现:

void SLTnsertAfter(SLTNode* pos, SLDataTyep x)//在指定位置之后插入数据
{
	assert(pos && pos->next);
	SLTNode* newnode = SListBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;

}

这里我们要注意的是:断言这里不光pos不能为空。就连pos下一个节点都不能为空,如果为空了,那就没有地方插入了。

二、删除节点

删除节点呢?我们也分为了:尾删,头删,删除pos节点,删除pos之后的节点

1.尾删

在这里插入图片描述
由图可以看出如果我们释放了prev指向的节点,那么3这个节点指向下个节点的指针就成了野指针了,那么这时我们就需要引入一个指针pcur,手动把他置为空。来看看代码的实现:

void SLTPopBack(SLTNode** pphead)//尾删
{
	assert(*pphead && pphead);
	if ((*pphead)->next == NULL)//当一个节点时。
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = *pphead;
		SLTNode* pcur = *pphead;
		while (prev->next)
		{
			pcur = prev;
			prev = prev->next;
		}
		free(prev);
		prev = NULL;
		pcur->next = NULL;
	}
}

这里我们看见代码有两种情况:第一种呢就是只有一个节点,二另一种就时有多个节点时。那么,当只有一个节点时,为何不能使用第二种情况呢。其实呀,这里我们来分析一下代码。刚开始我们定义两个指向头节点的指针变量prev,pcur,当只有一个节点时,prev->next=NULL,所以就直接跳过循环到释放prev,然后把prev置为空,但是这里的pcur->next=NULL就有问题,那问题出在哪里呢?prev和pcur两个指针都是指向头节点,那么这时我们已经通过释放prev已经把头节点给释放了,那么这里pcur->next==NULL是不是有问题呀?都被释放了,那么哪里来的指向下一个节点的指针呢?所以这里我们要分为两种情况。
这里要注意的一点就是:在释放空间之后一定要把它置为空,要养成这样的习惯哦各位

2.头删

在这里插入图片描述
从图中我们可以看出,头插只需要把指向头节点的指针指向下一个节点就行。使得第二个节点成为新的头节点。但是这里呢我们不能直接释放头节点。那为什么呢?我们来思考一个问题,如果我们这里选择直接释放头节点,那么如果要找第二个节点的话,那么这时还找得到吗?答案是肯定找不到的,因为头节点都被释放掉了,那么指向下一个节点得指针也被释放了呀!那还怎么找得到呢?那么这时我们该怎么办呢?
先来看看代码实现:

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

由代码可以看出,想要解决这个问题的话,只需要 重新定义一个新的指针变量俩暂时储存下一个节点,然后在释放头节点,然后在把头节点指向下一个节点使他成为新的节点。

3.删除pos节点

在这里插入图片描述
从图中可以看出,删除pos节点也很简单,但是这里我们需要知道pos之前和pos后一个的节点,pos之后的节点其实就是pos->next嘛,那么pos之前的节点呢?所以呀我们这里又要引入一个新的指针变量prev来找到pos之前的节点的指针指向pos之后的节点。

void SLTErase(SLTNode** pphead, SLTNode* pos)//删除pos节点
{
	assert(*pphead && pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	SLTNode* prev = *pphead;
	while (prev->next!=pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

从代码我们可以发现这里为啥还要调用一次头删函数呢?这里是因为pos在头节点的时候。这里的话就要调用一次头删函数。

4.删除pos之后的数据

在这里插入图片描述
通过图片我们可以看我们只需要让pos->next指向pos->next->next就可以完成删除的效果。但是我们这里该怎样实现代码呢?不能这样呢?

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	pos->next = pos->next->next ;
	free(pos->next );
	pos->next = NULL;
}

仔细看如果我们这里这里这样实现代码的话,那我们这里释放的不就是pos下下个节点嘛。也就是我们这里把4给释放了,所以这里并没有删除3这个节点。那该咋办呢?其实这里也需要一个临时变量。正确的是这样的:

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* del = pos->next;
	pos->next  = del->next;
	free(del);
	del = NULL;
}

定义一个临时变量,这样就不会冲突了。

三、链表的打印和查找

1.查找

SLTNode* SLFind(SLTNode* phead,SLDataTyep x)//查找
{
	assert(phead);
	SLTNode* prev = phead;
	while (prev)
	{
		if (prev->data == x)
		{
			return prev;
		}
		prev = prev->next;
	}
	return NULL;
}
int main()
{
	SLTNode* ret = SLFind(plish, 3);
	if (ret == NULL)
	{
		printf("没有找到\n");
	}
	else
	{
		printf("找到了\n");
	}
	return 0;
}

2.打印

void SListPrint(SLTNode* phead)//打印
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

3.链表的销毁

void SListDestory(SLTNode** pphead)//销毁链表
{
	assert(*pphead && pphead);
	SLTNode* pcur = pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
}

四、链表的代码实现与展示

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataTyep;
typedef struct SListNode
{
	SLDataTyep data;//数据节点
	struct  SListNode* next;//指向下一个节点的指针	
}SLTNode;
SLTNode* SListBuyNode( SLDataTyep x)//申请一个新的节点
{

	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail\n");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
void SLPushBank(SLTNode** pphead, SLDataTyep x)//尾插
{
	assert(pphead);
	SLTNode* newnode = SListBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;//链表为空的情况
	}
	else
	{
		SLTNode* ptail = *pphead;
		while (ptail->next )
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}
void SListPushFront(SLTNode** pphead, SLDataTyep x)//头插
{
	assert(pphead);
	SLTNode* newnode = SListBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataTyep x)//在指定位置插入数据
{
	assert(*pphead && pphead);
	assert(pos);
	SLTNode* newnode = SListBuyNode(x);
	if (*pphead == pos)
	{
		SListPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}

void SLTnsertAfter(SLTNode* pos, SLDataTyep x)//在指定位置之后插入数据
{

	assert(pos && pos->next);
	SLTNode* newnode = SListBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
void SLTPopBack(SLTNode** pphead)//尾删
{
	assert(*pphead && pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = *pphead;
		SLTNode* pcur = *pphead;
		while (prev->next)
		{
			pcur = prev;
			prev = prev->next;
		}
		free(prev); 
		prev = NULL;
		pcur->next = NULL;
	}
}
void SLTPopFront(SLTNode** pphead)//头删
{
	assert(*pphead && pphead);
	SLTNode* prev = (*pphead)->next;
	free(*pphead);
	*pphead = prev;
}
void SLTErase(SLTNode** pphead, SLTNode* pos)//删除pos节点
{
	assert(*pphead && pphead);
	assert(pos);
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	SLTNode* prev = *pphead;
	while (prev->next!=pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}
void SLTEraseAfter(SLTNode* pos)//删除pos之后的数据
{
	assert(pos);
	SLTNode* del = pos->next;
	pos->next  = del->next;
	free(del);
	del = NULL;
}
SLTNode* SLFind(SLTNode* phead, SLDataTyep x)//查找
{
	assert(phead);
	SLTNode* prev = phead;
	while (prev)
	{
		if (prev->data == x)
		{
			return prev;
		}
		prev = prev->next;
	}
	return NULL;
}
void SListPrint(SLTNode* phead)//打印
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}
void SListDestory(SLTNode** pphead)//销毁链表
{
	assert(*pphead && pphead);
	SLTNode* pcur = pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
}
int main()
{
	SLTNode* plish = NULL;
	SLPushBank(&plish, 1);
	SLPushBank(&plish, 2);
	SLPushBank(&plish, 3);
	SLPushBank(&plish, 4);
	SListPrint(plish);
	SListPushFront(&plish, 99);
	SListPrint(plish);
	/*SLTNode* ret = SLFind(plish, 3);
	SLTInsert(&plish, ret, 9);
	SListPrint(plish);
	SLTNode* ret = SLFind(plish, 2);
	SLTnsertAfter(ret, 11);*/
	SLTPopBack(&plish);
	SListPrint(plish);
	return 0;
}

总结

这里可能小编有些地方没有讲到的很详细,希望大家理解,我会好好努力,争取写出更好的文章,努力总会有回报的,加油!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值