番茄味城市甜菜头像
C语言:贪吃蛇前奏曲——链表的创建与维护封面图

C语言:贪吃蛇前奏曲——链表的创建与维护

下一文章将会介绍贪吃蛇小游戏的实现,这一章节主要介绍写游戏需要用到的数据结构——链表


一、什么是链表

链表是一种线性的数据结构,由一系列的节点组成,每一个节点包含两部分:

● 数据域:存放数据的部分

● 指针域:存放指向下一个节点的指针

如图:

上图中一个方框就是一个节点,如地址0x0012FFB0,该节点中存放的有数据和指向下一个节点的指针,图中plist我们可以形象地理解为“头指针”(注意这并不是真正的头指针,仅仅只是便于理解),每个节点在内存上的分布并非是连续的,他们通过每个节点存放的指针连接起来,每个节点都存放着指向下一个节点的地址,需要注意的是最后一个节点中存放的地址是NULL。

节点的结构定义

如下

sturct SListNode
{
    int data;                //节点数据
    struct SListNode* next;  //下一个节点的地址
};

二、为啥使用链表

在游戏运行的过程中,需要不断的维护蛇身每个位置的坐标,比如说,吃到食物需要新存入一个坐标数据,向前走一步需要需要改变每个坐标的位置等等,坐标的维护就通过创建链表来实现。

链表的优点:

● 使用灵活:想加就加,想减就减,不用把整块数据摞来摞去,插入删除都是O(1)。

● 使用方便:插入和删除数据都只需修改指针指向就行了,无需修改其他节点的位置。

● 永远不会装不下:想加数据就直接往后加就行了,不需要提前知道最多有多少数据需要存储,也不需要想数组那样提前申请多少空间。

三、链表的创建与维护

链表节点的创建

链表的节点的结构一般是在头文件中定义的,因此我们先创建一个头文件,就叫SList.h(这里的S就是single(单身的)单身的朋友应该很熟悉,SList就是单链表啦),在里面写上节点的定义

//SList.h
typedef int SLTDataType;
//这里弄是为了方便修改链表中的数据类型
//定义节点
typedef struct SLTNode
{
SLTDataType data;
struct SLTNode* next;
}SLTNode;

函数的声明:

除此之外,头文件也要包含维护该链表的函数申明(即对链表的数据进行增删查改的函数),同时也包含上要用到的头文件吧:

//SList.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SLTNode
{
SLTDataType data;
struct SLTNode* next;
}SLTNode;
//打印单链表
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);

这里的SLTNode就是函数的节点类型,phead就是指链表头的头指针,就是要插入或寻找的数据。

这函数些就是我们即将要对链表进行的操作了,我们一个一个来实现吧

尾插实现

这里需要创建一个源文件用于,存放函数的定义,就取名叫SList.c吧

SList.c

//尾插节点
void SLTPushBack(SLTNode** pphead, SLTDataType x);

尾插就是在连表的最后插入数据

SLTNode** pphead:头指针的地址(二级指针)。因为链表可能为空,或者插入操作可能需要修改头指针本身,所以必须传二级指针。
SLTDataType x:要插入的新数据。

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);//检查空指针
//申请空间
SLTNode* pnew = (SLTNode*)malloc(sizeof(SLTNode));
if (!pnew)
{
	perror("malloc");
	exit(1);
}
//初始化新节点data,next
pnew-&gt;data = x;
pnew-&gt;next = NULL;

//处理空链表的情况
if (*pphead == NULL)
{
	//修改首指针
	*pphead = pnew;
	return;
}
//有节点
else {
	//遍历链表,找到前驱节点,即最后一个节点
	SLTNode* prev = *pphead;
	while (prev-&gt;next)
	{
		prev = prev-&gt;next;
	}
	//链接前驱节点
	prev-&gt;next = pnew;
}
}

 assert(pphead):断言检查。确保传入的指向头指针的地址不是空指针

1. 申请节点:

SLTNode* pnew = (SLTNode*)malloc(sizeof(SLTNode)):在堆区申请一块内存,用来存放新的节点。

2. 初始化新节点

pnew->data = x:将传入的数据 x 存入新节点。

pnew->next = NULL:由于是尾插,新节点将成为新的最后一个节点,所以必须把它的指针域显式置为 NULL(对应图片中“记得把节点的存放的指针置为空”)。

3. 分情况处理

情况一:链表为空(if (*pphead == NULL))

如果头指针指向空,说明此时链表中还没有任何数据节点。
操作:*pphead = pnew 直接将头指针指向刚刚创建的新节点。

情况二:链表非空(else 分支)

如果链表里已经有数据,就需要“找尾巴”和“链接”。
操作:找尾(遍历链表) SLTNode* prev = *pphead:定义一个遍历指针 prev,从头节点开始。
while (prev->next) { prev = prev->next; }:依次往后走,直到 prev 指向的节点的 next 为空。 此时prev 刚好停在最后一个节点上。

链接(修改指针):prev->next = pnew;:把当前最后一个节点(prev)的 next 指针指向新 节点(pnew)。至此,新节点成功被连接到链表的尾部。

头插实现

//前插节点
void SLTPushFront(SLTNode** pphead, SLTDataType x);

头插就是在链表的最前面插入数据

这里的参数和上面是差不多的,这里就不过多赘述了。

//头插节点
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);//检查空指针
//申请空间
SLTNode* pnew = (SLTNode*)malloc(sizeof(SLTNode));
if (!pnew)
{
	perror("malloc");
	exit(1);
}
//修改新节点data,next
pnew-&gt;data = x;

//处理无节点的情况
if (*pphead == NULL)
{
	//定义首指针
	*pphead = pnew;
	(*pphead)-&gt;next = NULL;
	return;
}
//有节点
else {
	//链接首节点
	SLTNode* nest = *pphead;
	*pphead = pnew;
	pnew-&gt;next = nest;
}
}

解释:

1. 申请空间:申请堆区空间(malloc),用于存放新节点,并检查申请是否成功(if (!pnew))。

2. 初始化新节点:在申请的空间中放入要插入的数据 pnew->data = x;。

接下来同样是分两种情况处理:

情况一:if (链表中没有数据,即 *pphead == NULL)

3. 修改头指针,让头指针指向新生成的节点 *pphead = pnew。

4. 因为是唯一的节点,记得把它的指针域置为空 (*pphead)->next = NULL。

情况二、else (链表中有数据)

3. 因为我们要把新节点放到最前面,所以需要先保存原来的头节点。定义临时指针 SLTNode* next = *pphead;

4. 修改头指针的指向,让它指向新生成的节点 *pphead = pnew;。

5. 将新节点的指针指向刚才保存的旧头节点 pnew->next = nest;,至此链接完成。

尾删节点

//尾删节点
void SLTPopBack(SLTNode** pphead);

就是在链表尾部删除节点

//尾删节点
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//找到前置节点和尾节点
	SLTNode* prev = *pphead;
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
//处理只有一个节点的情况
if (ptail == prev)
{
	free(ptail);
	*pphead= NULL;
}
//多个节点
else{
	free(ptail);
	prev-&gt;next = NULL;
}
}

1. 寻找前驱节点和尾节点

SLTNode* prev = *pphead; 和 SLTNode* ptail = *pphead;:定义两个指针,一开始都指向头节点。

while (ptail->next) 循环内部:

prev = ptail; 让 prev 紧跟在 ptail 后面。

ptail = ptail->next; 让 ptail 继续向后走。

循环结束条件:当 ptail->next 为 NULL 时循环停止。此时 ptail 指向尾节点,而 prev 刚好指向倒数第二个节点(尾节点的前驱)。

2. 分情况处理

情况一:链表只有一个节点(if (ptail == prev))

如果是单节点,ptail 和 prev 都会指向同一个节点(首尾相同)。

free(ptail):直接释放这个唯一节点的内存。

*pphead = NULL:因为节点被释放了,原来的头指针就变成了“野指针”。必须将头指针置为 NULL,表示此时链表已空。

情况二:链表有多个节点(else)

此时 ptail 指向尾节点,prev 指向倒数第二个节点。

free(ptail);:释放尾节点的内存空间。

prev->next = NULL;:将新的尾节点(原来的倒数第二个节点)的指针域置为空,确保结 构完整(它现在是新的尾巴了)。

头删节点

//头删节点
void SLTPopFront(SLTNode** pphead);

就是在链表头部删除节点

//头删节点
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//处理只有一个节点的情况
	if ((*pphead)->next == NULL)
	{
		SLTPopBack(pphead);
		return;
	}
	//多个节点
	else{
		//保存新的首节点地址
		SLTNode* next = (*pphead)->next;
		//释放空间
		free(*pphead);
		//定义首指针
		*pphead = next;
	}
}

1. 分情况处理

情况一:链表只有一个节点(if ((*pphead)->next == NULL))

SLTPopBack(pphead):在只有一个节点的情况下,头删和尾删是同一个操作。代码直接复用了之前写好的尾删函数 SLTPopBack 来处理

情况二:链表有多个节点(else 分支)

如果有多个节点,删除头节点后,我们需要让第二个节点成为新的头节点。

SLTNode* next = (*pphead)->next;:先保存新头节点的地址。定义一个临时指针 next,让它指向原来的第二个节点。如果不先保存,一旦释放了旧的头节点,我们就找不到后面的链表了。

free(*pphead);:释放旧的头节点内存。

*pphead = nest:修改头指针,让它指向刚刚保存的新头节点(即原来的第二个节点)。

打印链表

//打印单链表
void SLTPrint(SLTNode* phead);

就是打印链表中每一个节点的数据

这里由于不用修改头指针和链表数据,索性使用一级指针吧

//打印
void SLTPrint(SLTNode* phead)
{
    //遍历链表并打印
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
    //表明最后的节点指向NULL
	printf("NULL\n");
}

循环遍历:
while (pcur):只要 pcur 不为空(还没有走到链表末尾),就继续循环。

printf("%d->", pcur->data);:打印当前节点的数据,并加上 -> 符号。

pcur = pcur->next;:指针向后移动,指向下一个节点。

打印出来的效果:1->2->3->4->NULL;

销毁链表

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

就是销毁创建的每一个节点,释放内存。

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

*pphead = NULL;
}

1. 定义遍历与辅助指针:

SLTNode* next = *pphead; 和 SLTNode* pcur = *pphead;:一开始都指向头节点。

2. 循环释放内存:

while (pcur):只要当前节点 pcur 不为空,就继续循环。

next = pcur->next;:在释放 pcur 之前,先把下一个节点的地址保存到 next 中。

free(pcur);:释放当前节点占用的内存。

pcur = next;:让 pcur 移动到刚才保存的下一个节点,准备下一轮释放。


心得体会:

到这里就结束了,已经是凌晨00:40了,写到这里真的是身心俱疲,实在写不下去了,至于没写的函数,贪吃蛇用不到的,我就不写了。唉,想要坚持下去真的不容易,但每次写完看到有人点赞,虽然可能都没认真看,人也不多,但想到有人支持我,都会莫名感动,我想这就是我坚持下去的动力吧。最近开学了,课是排的满满当当,有的还听不懂,只能调整为月更了。好吧,就不吐苦水了,翻这么久翻到这里也不容易,就来张图吧

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/lei_jiale/article/details/167282702

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--