单向链表的学习笔记1

前言介绍

在学习链表之前让我们来了解一下什么是链表,链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
那么我们用下图来方便理解:
在这里插入图片描述
从图中我们可以看出,链表包含一个头指针,指向我们开始存放数据的内存空间,在这个空间里面存放着数据以及一个地址,这个地址指向我们下一个链表的空间,从而将链表连接起来,最后一个空间的指针则为空,没有指向,这样就是我们一个完整的链表,实际中是没有箭头的,箭头只是我们方便理解,就是根据地址来寻找我们链表的下一个空间。

单向链表的实现

链表的创建

从上面的图解中,不难看出我们需要创建一个空间,既能存放我们的数据又要能存放我们的地址,这里就需要创建一个结构体变量如下:

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

data存放我们数据,next则为下一个链表空间的地址。

链表的增删查改

链表的头插与尾插

现在有了一个链表的架构,既然有了一份链表那么我们肯定希望可以往里面存储数据,我们默认新创建一个结构体变量的指针作为我们的头地址,这里我们将头地址置空,也就是我们的链表处于未创建的状态:

	SLTNode* plist = NULL;					//初始化链表头项的地址

接下来看看如何实现增加的功能:

新空间的创建

要想放入新的数据,我们是需要申请一个新空间的,这就是链表的优点之一,要多少就申请多少,不会存在空间浪费。

SLTNode* CreateSLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));		//创建新链表的空间
	if (newnode == NULL)
	{
		perror("SLTPushBack");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;						//新空间里的指针置空
	return newnode;
}

在申请新空间的过程中我们将数据放入我们的新空间,并将新空间里面的指针置空方便后续操作。

尾插的实现
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);								//防止传入参数错误
	SLTNode* newnode = CreateSLTNode(x);

	if (*pphead == NULL)
	{
		*pphead = newnode;						//如果当前的头地址为空,则将新创建的地址赋给头地址
	}
	else
	{
		SLTNode* tail = *pphead;				//找到当前链表的尾巴
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

尾插的实现分两步,如果当前链表没有空间,就直接将我们的新空间传给我们的头地址就行,如果链表中已经有数据了,则通过遍历,让tail遍历到链表的最后一个空间,然后让当前空间的指针指向我们的新空间即可。

头插的实现
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);								//防止传入参数错误
	SLTNode* newnode = CreateSLTNode(x);
	
	newnode->next = *pphead;		//将新空间里的指针赋为先前的头地址
	*pphead = newnode;				//再将当前空间置为头地址
}

头插的逻辑相对于尾插就简单很多了,将新空间的地址存入我们之前的头地址,再将头地址更改为我们现在的新空间,在头插的情况中我们不需要考虑链表是否已经创建。

链表的头删与尾删

链表的尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);								//防止传入参数错误
	assert(*pphead);
	if ((*pphead)->next == NULL)		//如果当前链表只有一个,释放当前的空间并将指针置空
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		//需要一个尾巴,以及指向尾巴前一个空间的指针
		SLTNode* tail = *pphead;
		SLTNode* prev = NULL;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		//找到尾巴空间后释放空间并将上一个空间的指针置空
		free(tail);
		tail->next = NULL;
		prev->next = NULL;
	}
}

同样在尾删的过程中我们需要判断当前链表是否只有一个空间或者一个以上的空间,根据不同的情况进行处理。一个空间的比较简单,多个空间需要考虑一个新的情况,因为我们链表的尾巴里的地址都是置空的,但是当我们删除尾巴之后,由于单向链表的单向性,我们是无法回头找到尾巴的前一个地址的,所以我们在这里定义多一个新的指针prev,专门记录尾巴的前一个,这样子在我们删除尾巴的位置后,再将prev里的指针置空我们就完成了尾删。

链表的头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);								//防止传入参数错误
	assert(*pphead);

	SLTNode* HeadNext = (*pphead)->next;		//记录头地址的下一个地址
	free(*pphead);
	*pphead = HeadNext;
}

头删相比尾删逻辑也很简单,先记录头地址的下一个地址,再将头地址释放,然后将头地址更改为我们事先记录的HeadNext就行了,这里就算是只有一个空间也是没问题的。

链表的查找

SLTNode* SLTNodeFind(const SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}

	return NULL;
}

链表的查找逻辑就比较简单了,遍历一遍链表,将查找到的地址返回即可。但是这里链表的查找万一有多个相同的数据我们遍历又只能找到第一个,如果我们想要后面的怎么办呢,这里我们可以用一个循环解决这个问题:

//查找功能的实现
	SLTNode* pos = SLTNodeFind(plist, 2);
	int i = 1;
	while (pos)		//可通过循环来查找到多个重复的数据
	{
		printf("第%d个2的位置:%p\n", i++, pos);
		pos = SLTNodeFind(pos->next, 2);
	}

另外链表的查找函数还可以实现一个额外的功能,那就是修改,因为我们对链表的操作都是通过指针,那么我们就可以通过查找函数返回的地址,在通过对指针的操作进行修改:

//修改功能的实现
	SLTNode* pos1 = SLTNodeFind(plist, 3);
	pos1->data = 30;

指定位置的插入与删除

前面介绍的插入与删除都是在头部与尾部进行的,如果我们想在任意位置进行插入与删除能不能也封装一个函数来完成呢,当然是可以的,下面即是代码的实现过程:

在链表指定的位置插入数据

指定位置插入函数实现有一个前提,那就是是要去寻找这个指定位置,我们得先找到我们想要插入的位置才能进行插入,所以外面还有一层代码是:

//插入功能的实现
	SLTNode* pos1 = SLTNodeFind(plist, 1);
	if (pos1)
	{
		SLTInsert(&plist, pos1, 10);
	}

下面就是我们指定位置插入函数的实现:

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);								//防止传入参数错误
	assert(pos);
	SLTNode* newnode = CreateSLTNode(x);
	if (*pphead == pos)
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		SLTNode* PosPrev = *pphead;
		while (PosPrev->next != pos)
		{
			PosPrev = PosPrev->next;		//找到pos位置前一个
		}
		PosPrev->next = newnode;
		newnode->next = pos;
	}
	
}

在这里这个实现的函数是在pos位置之前进行插入,有没有发现这种插入方式跟我们之前实现的哪种函数有些类似呢?这里其实有一部分逻辑是跟尾删类似,因为我们在pos之前插入一个新空间,肯定需要将pos的前一个指针指向我们的新空间,然后再将我们的新空间指向我们的pos,所以也是需要两个指针进行操作的,这样子就会对我们的程序产生效率损失,所以我们在指定位置插入时通常都是插入在pos位置之后,也就是接下来的函数:

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = CreateSLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

插入pos之后的逻辑那可就太简单了,首先将新空间指向原本pos指向的next,再将pos的next指向我们的新空间即可,这样效率也相比在pos前插入得到了提升。

在链表的指定位置删除

指定位置删除的逻辑跟指定位置插入是差不多的,我们同样是需要先寻找到这个指定的位置:

//删除功能的实现
SLTNode* pos1 = SLTNodeFind(plist, 1);
	if (pos1)
	{
		SLTEarse(&plist, pos1);
	}

删除函数的实现:

void SLTEarse(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);								//防止传入参数错误
	assert(pos);
	if (*pphead == pos)						//头删
	{
		*pphead = (*pphead)->next;
		free(pos);
		pos->next = NULL;
	}
	else
	{
		SLTNode* PosPrev = *pphead;			//记录pos前一个空间的位置
		while (PosPrev->next != pos)
		{
			PosPrev = PosPrev->next;
		}
		PosPrev->next = pos->next;
		free(pos);
		pos->next = NULL;
	}
}

这一个是删除指定位置的函数,有没有发现这一种方法也是需要双指针,跟我们刚刚指定位置前插是一个道理,都会产生效率上的损耗,所以我们也是比较常用下面一种删除方法,指定位置的后删:

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

这一个相对于上一个就逻辑简单很多了,而且效率也有提升。

链表的销毁

上述函数介绍完就轮到最后一个函数,当我们结束我们的进程时需要对我们申请的空间进行释放,不然将会造成内存泄露,所以就需要我们的销毁函数,在我们进行销毁之前需要思考一下,是不是简单的释放头地址就可以了呢?我们来看一下下面一幅图:
在这里插入图片描述
假如我们有这样一个链表,那么对他的头地址进行释放并置空之后会变成如下:
在这里插入图片描述
这里发现如果只是单纯的释放头地址,只是解决了一个空间的问题,并且会导致我们后续的空间找不到了无法释放,从而造成内存泄露,后果将会很严重,所以我们这里不能单纯的只是释放头地址,而应该将每一个空间都释放。
销毁函数如下:

void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);								//防止传入参数错误
	SLTNode* cur = NULL;
	SLTNode* STLNext = *pphead;
	while (STLNext)
	{
		cur = STLNext;
		STLNext = STLNext->next;
		free(cur);
		cur->next = NULL;
	}
	*pphead = NULL;
}

搞清楚逻辑这个销毁函数就很简单了,我们使用两个指针,一个记录当前空间,一个记录下一个空间,这样我们就可以在释放当前空间的同时还能找到下一个空间从而完成逐个释放。

链表的打印

void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

这是链表的打印函数,方便我们在实现增删查改函数的过程中判断我们的函数是否正确,当然我觉得用调试看是更好的,因为当函数功能实现错误时,打印功能并不能准确帮我们找出错误,但是进入调试后错误其实是一目了然的(前提是自己要逻辑清晰)

结语

在单向链表的学习过程中,其实不难发现链表相对于顺序表是各有优劣的,一方面链表内存利用率高,不会浪费内存、大小没有固定,拓展很灵活,但是缺点也很明显,那就是方向是单一的,不能倒着找数据,查询效率也低,要通过遍历。在链表这一块学习也是感受到对指针的运用要很灵活,要比较熟练的掌握指针。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

葛叶灬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值