【线性表】单链表详解(C语言版)

目录

引言

 一、定义

二、开辟新结点

三、插入

1.头插

2.尾插

(1)没有结点

(2)已有结点

(3)尾插总结

3.中间插入

(1)在pos位置前插入

(2)在pos位置后插入

四、删除

1.头删

2.尾删

(1)只有一个结点

(2)有多个结点

(3)尾删总结

3.中间删除

(1)删除pos位置结点

(2)删除pos后结点

五、查找

六、修改

结尾


ID:HL_5461 

引言

在开始动手写之前,我们先来了解链表是什么:

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

当然,链表有单链表、双向链表、带头链表、循环链表等多种,这一篇我们只讨论无头非循环的单链表。该链表用图片形象化表示,大概长这样:

 一、定义

不同于顺序表,链表我们只通过结构体定义一个结点,这个结点包含两个东西:一个是该节点存储的数据data,一个是指向下一个结点的指针next。我们的链表就是通过这些指针指向来把一个个结点连成一个链表。

 图示

typedef int SLTDataType;//定义SLTDataType为int,方便以后修改存储的数据类型

typedef struct SLTNode
{
	SLTDataType data;//结点数据
	struct SLTNode* next;//指向下一个结点的指针
}SLTNode;

代码

二、开辟新结点

链表在没有存放任何数据时,只有一个指向空的指针,也就说,我们想要创建一个链表,只需要在主函数中输入“SLTNode* pList = NULL;”,我们就算已经创建好了一个尚未存放数据的链表。这似乎比顺序表方便多了,因为我们不需要写一个函数对它进行初始化了。但是,我们需要写一个开辟新结点的函数,同时,这个函数也方便了以后对于链表进行插入。

首先我们直接使用malloc函数为结点开辟一个结点结构体大小的空间,同时返回该结点的指针,将之赋值给newnode。当然,对于malloc函数开辟空间,很重要一点就是判断开辟是否失败,即指针是否成功,失败结束程序,成功我们就可以将要存储的数据存进新开辟的结点空间中的data里,同时为了避免使用野指针,将该结点的next置为空。

来看代码:

SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//用malloc函数为结点开辟空间

	if (newnode == NULL)//如果开辟失败
	{
		perror("BuySListNode");//打印开辟失败
		exit(-1);//程序以非正常形式结束
	}
	else//如果开辟成功
	{
		newnode->data = x;//将数据存入data
		newnode->next = NULL;//next指针置空
	}

	return newnode;//返回结点指针
}

三、插入

1.头插

直接上图讲解:

运用BuySListNode函数开辟新结点,定义一个结构体指针newnode,将返回的该结点的指针存在newnode里面。此时,新开辟的结点被newnode所指向,同时该结点的next指向空。

令新结点的next指向头结点指向的位置。

令头指针指向新结点。

 插入结点指针改变的顺序很重要,我们只能先改变新结点的next令它指向头结点,再令头指针指向它,而不能先令头指针指向它,那样我们就找不到后面的链了。来,我们还是用图说话!

注意,下面这个是一个错误图例!!

这里,我们先改变的是pList指向的地址,不难发现,后面的一大长条链我们都找不到了。当然,这后果可不是单纯“找不到”这么简单,由于无法找到后面这些结点的地址,而这些结点又都是malloc动态开辟的,我们无法将其释放,这就造成了内存泄漏。嗯~恭喜你创造了一个电脑病毒~(bushi) 

看了上面的解说你是不是觉得自己会了【奸笑】,好吧,那咱们来看一个错误样例:

//这是一个错误示范
void SLTPushFront(SLTNode* phead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
	newnode->next = phead;//新结点的next指向头指针指向的结点
	phead = newnode;//令头指针指向新节点
}

这段代码咋一看好像没啥问题,没毛病,都是咱前面讲的思路。来来来,觉得没毛病的让我先问你一个问题:pList的指向真的改变了吗?

许多人看到SLTNode* phead就觉得自己传了指针过去用phead接收,所以想当然地认为pList已经改变了,实际真的如此吗?我们画图来理解:

首先我们要明确一点:所谓指针指向某一块区域,实际是指该指针变量存放的是那块区域的地址。

这里,我们假设头结点地址为0x000000417,也就说SLTNode*类型的变量pList存放的值为0x000000417。此时我们调用SLTPushFront(头插)函数,同时该函数创建一个临时变量phead来存放pList的值,同样是0x000000417。

 在执行函数过程中,我们修改了phead的值为新结点的地址,假设为0x000000410。此时改变的只是形参,并没有改变pList的值。

总结一下:虽然phead是SLTNode*类型的变量,是一个指针,但我们要改变的是pList这个指针变量的值,所以要传的是pList的地址,也就说要传一个二级指针。

学废了吗~来看正确代码:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
	newnode->next = *pphead;//新结点的next指向头指针指向的结点
	*pphead = newnode;//令头指针指向新节点
}

2.尾插

对于尾插,我们有必要分两种情况讨论:一种是链表中没有结点的,一种是链表中有结点的。图解和代码我都会分这两种情况来讨论。

(1)没有结点

没有结点的情况其实和头插类似,话不多说,上图:

 运用BuySListNode函数开辟新结点,定义一个结构体指针newnode,将返回的该结点的指针存在newnode里面。此时,新开辟的结点被newnode所指向,同时该结点的next指向空。

为了与前面的头插保持一致,我们不妨也令新结点的next指向头结点指向的位置。当然这一步其实没太大必要,因为无论变不变next都为NULL。

 令头指针指向新结点。

我们先实现一下这部分代码,最后再对两部分代码进行合并。

当然,正如前面头插强调的,别忘了使用二级指针!

//这段代码不全
if (*pphead == NULL)//如果头指针指向空,即链表内无结点
{
	SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
	newnode->next = *pphead;//新结点的next指向头指针指向的结点
	*pphead = newnode;//令头指针指向新节点
}

(2)已有结点

没啥可说的,直接上图哈~

 运用BuySListNode函数开辟新结点,定义一个结构体指针newnode,将返回的该结点的指针存在newnode里面。此时,新开辟的结点被newnode所指向,同时该结点的next指向空。

 令新结点的next指向原来尾结点的next指针指向的位置。同样没啥必要,但是咱养成好习惯哈~

 令原来尾结点的next指针指向新结点的位置。

但是,怎么找到尾结点的位置呢?链表不像顺序表,我们要想找到最后一个,需得设一个指针让它从头开始遍历,直到找到个结点的next为NULL才算找到了最后一个结点。我们看图。

创建一个SLTNode*类型的指针tail,刚开始令它等于*pphead也就是等于PList,指向头结点。

放入一个循环,令tail = tail->next,即指针不断向后一个结点走,直到最后一个结点停下,由此,循环结束的条件为tail->next =  NULL。

嗯~当然我们还是先来看看坑~下面是一个错误示例:

//这是一个错误示范
SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
SLTNode* tail = *pphead;//tail指向头结点
while (tail)//tail不为空时
{
	tail = tail->next;//往下一个结点走
}
tail = newnode;

这段代码和我们之前讨论的区别就在tail最后所停的位置,我们所讨论的,tail停在了最后一个结点,这里的tail停在了最后一个结点的后一个位置。我们还是画图来辅助理解。

错误示范中,while循环的结束条件是tail==NULL,循环结束,此时tail停留在了最后一个结点的next位置上,也就说,tail的值为NULL。

此时我们再令tail = newnode,所改变的只是tail的指向,让它的值由原来的NULL变为了新结点的地址,并没有把它“连到”原来的链上。

所以,有一点还是有必要强调的:改变结构体,要使用结构体指针!

OK,来看正确代码:

//这段代码不全
else//链表内有结点
	{
		SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
		SLTNode* tail = *pphead;//tail指向头结点
		while (tail->next)//tail的next不为空时
		{
			tail = tail->next;//往下一个结点走
		}
		newnode->next = tail->next;//新结点指向最后一个结点的next
		tail->next = newnode;//最后一个结点指向新结点
	}

(3)尾插总结

对于尾插,我们需要牢记必须分为已有结点和未有结点两种情况讨论!

话不多说,我直接把尾插的完整代码放出来:

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点

	if (*pphead)//如果头指针指向空,即链表内无结点
	{
		newnode->next = *pphead;//新结点的next指向头指针指向的结点
		*pphead = newnode;//令头指针指向新节点
	}
	else//链表内有结点
	{
		SLTNode* tail = *pphead;//tail指向头结点
		while (tail->next)//tail的next不为空时
		{
			tail = tail->next;//往下一个结点走
		}
		newnode->next = tail->next;//新结点指向最后一个结点的next
		tail->next = newnode;//最后一个结点指向新结点
	}
}

3.中间插入

中间插入我们分成在pos位置前插入和在pos位置后插入两种情况讨论。对于前插和后插,我认为最大的不同就是一个需要遍历链表找pos位置一个不需要,还有一个有头插情况一个有尾插情况。当然,这些都让我们接下来逐个分析。

(1)在pos位置前插入

插入前,我们先要设置一个指针prev遍历链表来找pos位置,当然prev所停的位置有些讲究,我们后面画图讨论。然后我们将新开辟的结点的next指向pos,再将pos前面的指针指向新结点。

创建结构体指针prev,刚开始指向头结点,然后不断让prev=prev->next,让它在链表中遍历。

在pos的前一个结点停下,即跳出循环的条件为prev->next==pos。

这里要特别注意,跳出循环的条件不能是prev==pos,否则会出现下面这种情况:

若跳出循环的条件是prev==pos,就无法将pos结点的上一个结点的next指向新结点,因为我们不能知道上一个结点的地址,也就无法通过上一个结点的地址找到对应的next。链表指针只能顺着next的指向从前往后走,不能通过后一个找到前一个。

当prev停在了pos的前一个结点,此时我们再让 新结点的next指向pos。

然后让prev的next指向新结点。

你以为pos前插入结束了吗?恭喜你,想多了~我们不妨考虑一下,如果pos是头结点呢?也就说,我们得分情况,上面讨论的是pos不是头结点的情况,如果pos为头结点,我们得实现结点的头插。头插前面讲过,在此不再赘述,直接看代码。 

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);//pphead不为空
	assert(pos);//pos不为空

	SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点

	if (*pphead == pos)//如果头指针指向pos,为头插
	{
		newnode->next = pos;//新结点的next指向pos结点
		*pphead = newnode;//令头指针指向新节点
	}
	else//头指针不指向pos
	{
		SLTNode* prev = *pphead;//prev指向头结点
		while (prev->next != pos)//prev的next不为pos结点时
		{
			prev = prev->next;//往下一个结点走
		}
		newnode->next = pos;//新结点指向最后一个结点的next
		prev->next = newnode;//最后一个结点指向新结点
	}
}

(2)在pos位置后插入

后插比前插方便得多,我们甚至不需要遍历链表。后插不可能出现头插情况,因为所插位置的前面至少存在一个结点,但可能存在pos为最后一个结点,即尾插情况,不过无需分开讨论。

 直接看代码。

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);//pos不为空
	SLTNode* newnode = BuySListNode(x);//用BuySListNode函数开辟新结点
	newnode->next = pos->next;//newnode指向pos的下一个结点
	pos->next = newnode;//pos指向newnode
}

四、删除

1.头删

 我们首先使用一个结构体指针类型的变量head记录下来要删除的头结点。这里的记录是为了后面的释放空间。

改变头指针指向,令它越过头结点指向后一个结点 。因为这里需要改变头指针的值,所以这里的函数应该传二级指针。

释放head所指向的空间。 

看代码:

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);//二级指针不为空
	assert(*pphead);//链表不为空

	SLTNode* head = *pphead;//创建指针变量使它指向头结点
	*pphead = (*pphead)->next;//头指针指向头结点的下一个
	free(head);//释放头结点空间
}

2.尾删

(1)只有一个结点

尾删和尾插一样,同样得考虑是否只有一个结点的问题,我们分情况来讨论,先来看只有一个结点的情况:

 只有一个节点的情况其实就是头删,我们用tail指针记录结点位置然后修改头指针再对tail进行释放。当然这里其实可以直接将PList所指结点释放再对PList置空。但为了和之前保持一致,我们不妨多走两步。

 修改PList指向。

 释放tail空间。

来看只有一个结点情况的代码:

//这段代码不全
if ((*pphead)->next == NULL)//只有头结点一个结点
{
    SLTNode* tail = *pphead;//令tail指针指向头结点
	*pphead = (*pphead)->next;//指向后一个
	free(tail);//释放tail空间
}

(2)有多个结点

再来看有多个结点的情况:

我们仍然是将tail指针从前往后遍历,直到找到倒数第二个结点。注意这里不可遍历到最后一个结点!

释放尾结点,并将tail的next置空。

来看这部分代码:

//这段代码不全
else//有多个结点
{
	SLTNode* tail = *pphead;//令tail指针指向头结点
	while (tail->next->next)//不是倒数第二个结点
	{
		tail = tail->next;//向后遍历
	}
	free(tail->next);//释放尾结点
	tail->next = NULL;//tail的next指针置空
}

(3)尾删总结

直接看代码吧~

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);//二级指针不为空
	assert(*pphead);//链表不为空

	SLTNode* tail = *pphead;//令tail指针指向头结点
	if ((*pphead)->next == NULL)//只有头结点一个结点
	{
		*pphead = (*pphead)->next;//指向后一个
		free(tail);//释放tail空间
	}
	else//有多个结点
	{
		SLTNode* tail = *pphead;//令tail指针指向头结点
		while (tail->next->next)//不是倒数第二个结点
		{
			tail = tail->next;//向后遍历
		}
		free(tail->next);//释放尾结点
		tail->next = NULL;//tail的next指针置空
	}
}

3.中间删除

(1)删除pos位置结点

删除pos位置结点还是要分删除头结点和删除中间结点两种情况讨论,前面已有详细说明,这里就一笔带过。

先来看删除头结点的情况,当然,嫌麻烦的也可以直接调用SLTPopFront函数。

令头指针指向pos的后一个结点。 

释放pos结点。 

 再来看删除中间结点情况:

 定义prev指针使其遍历到pos前一个。

prev的next指针指向pos的后一个结点。 

释放pos结点。 

look代码:

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);//二级指针不为空
	assert(*pphead);//链表不为空
	assert(pos);//pos不为空

	if (pos == *pphead)//pos为头结点
	{
		*pphead = pos->next;//头指针指向pos的下一个结点
		free(pos);//释放pos
	}
	else//pos为中间结点
	{
		SLTNode* prev = *pphead;//令prev指针指向头结点
		while (prev->next != pos)//不是pos的前一个结点
		{
			prev = prev->next;//向后遍历
		}
		prev->next = pos->next;//prev指向pos的后一个结点
		free(pos);//释放pos
	}
}

(2)删除pos后结点

后删要简单的多,我们无需考虑头结点情况,不用分开讨论。

使用posNext指针记录pos的后一个结点,即要删除的结点。

使pos指向posNext的后一个结点。

释放posNext结点。 

没啥可讲的,so easy~来看代码!

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);//pos不为空
	assert(pos->next);//要删的结点不为空

	SLTNode* posNext = pos->next;//posNext为pos的后一个节点
	pos->next = posNext->next;//pos指向posNext的后一个结点
	free(posNext);//释放posNext
	posNext = NULL;//指针置空(可有可无)
}

五、查找

查找的话,直接一个指针从头遍历到尾,有就返回该值所在地址,没有则返回空指针。上代码叭~

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;//从头遍历
	while (cur)//cur不为空
	{
		if (cur->data == x)//找到了
		{
			return cur;//返回此时地址
		}
		else//没找到
		{
			cur = cur->next;//往后找
		}
	}
	//遍历结束仍未找到
	return NULL;//返回空指针
}

六、修改

看代码:

void SLTModify(SLTNode* pos, SLTDataType x)
{
	assert(pos);//pos不为空
	pos->data = x;//将pos位置的数据修改为x
}

结尾

无头结点非循环单链表到此就结束啦~完整代码照例放在了我的码云,欢迎前来串门:

class_c: 课上要认真鸭~ - Gitee.com

目前咱不确定我还会不会再开一篇文章总结一下单链表的所有代码(无他,我懒),如果更了,我会把链接放到下面哒~

Finally,若有错误,欢迎大家批评斧正!

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

是兰兰呀~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值