# 用C语言实现一个链表(二)

用C语言实现一个链表(二)

在上期内容中,我们探讨了实现一个链表的准备工作以及一些功能——创建新结点,尾插数据,尾删数据,遍历的代码。其中,详细说明了结构体以及二级指针的使用,本期内容,我们将继续讨论其他一些功能的代码实现。
链表结构图

一、链表的实现(代码+分析)

1.链表的头部数据插入(头插)

头部插入数据相对轻松,只要理解指针的链接关系即可,创建的新结点的指针域(next)应当指向头结点,而原来指向头结点的指针需要指向新结点,代码如下:

void LinkPushFront(LinkNode** pphead, DataType x)
{
	assert(pphead);
	LinkNode* newnode = CreateLinkNode(x);//创建新结点,将其赋给同型结构体指针newnode
	newnode->next = *pphead;//这时新节点的指针域应该指向传入的头部指针*pphead
	*pphead = newnode;//此时头部就是刚插入的新结点newnode,*pphead改变指向,指向链表的头——newnode
}

2.链表的头部数据删除(头删)

在删除头部数据时,需要注意的,如果直接把头指针指向的空间释放掉,那么后面的一串结点就找不到了,因为后面一串结点的起始位置是由头结点的next找到的,因此先要用一个临时的指针tmp保存一下头结点后面的next,代码如下:

void LinkPopFront(LinkNode** pphead)
{
	assert(pphead);//首先,头指针的地址不能为空
	assert(*pphead);//其次,头指针本身不能是空指针
	LinkNode* tmp =(*pphead)->next;//临时变量将头结点后面的结点保护起来
	free(*pphead);//这样在释放头指针指向的空间后还能找到后面的结点在哪里
	*pphead = tmp;//最后头指针指向第二个结点
}

3.链表特定位置的寻找

根据结点的数据域,通过链表的遍历,找到结点所在的位置返回一个指针,代码如下:

LinkNode* LinkFind(LinkNode* phead, DataType x)
{
	LinkNode* cur = phead;//创建一个同型结构体指针变量用于遍历查找
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;//返回的是一个指针,指向要查找的结构体
		}
		cur = cur->next;//cur移向下一个结点
	}
	return NULL;//如果没有找到,返回空指针
}

4.链表特定位置之前的数据插入

链表数据插入于删除,关键还是要理清楚指针的链接关系,控制循环条件。在这里,之前写的LinkFind函数就起到了作用,比如说想在链表数据3之前插入一个数据6,就要先调用LinkFind函数,找到数据域为3 的结点所在地址,然后传给这里的LinkInsertFront插入函数,代码如下:

void LinkInsertFront(LinkNode** pphead, LinkNode* pos, DataType x)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		LinkPushFront(pphead, x);//如果刚好是在首元素前插入,调用之前的头插函数即可
	}
	else
	{
		LinkNode* newnode = CreateLinkNode(x);//插入数据就要创建新结点
		LinkNode* front = *pphead;//创建同型指针变量front,寻找pos之前的那个结点
		while (front->next != pos)//pos前一个结点的特征就是它的指针域(next)指向的是pos
		{
			front = front->next;//向后遍历
		}		
		front->next = newnode;//前一个结点的指针域不再指向pos,而是指向新插入的结点
		newnode->next = pos;//新插入的结点的指针域指向的是pos
	}
}

5.链表特定位置数据的删除

这里还是指针的链接关系,往往和LinkFind函数一起使用,代码如下:

void LinkErase(LinkNode** pphead, LinkNode* pos)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		LinkPopFront(pphead);//刚好是第一个结点,调用之前的头部数据删除函数
	}
	else
	{
		LinkNode* front = *pphead;//创建一个同型结构体指针变量,用于寻找pos之前的结点
		while (front->next != pos)
		{
			front = front->next;//向后查找
		}		
		front->next = pos->next;//pos前一个结点的指针域指向pos后一个结点
		free(pos);//释放pos的空间		
	}
}

6.链表的销毁

在销毁前一个结点时,要先保存好下一个结点的位置,这样在前一个结点释放空间后还能找到下一个结点,代码如下:

void LinkDestroy(LinkNode** pphead)
{
	assert(pphead);
	LinkNode* cur = *pphead;
	while (cur != NULL)
	{
		LinkNode* tmp = cur->next;
		free(cur);
		cur = tmp;
	}
	*pphead = NULL;
}

7.运行测试

测试代码如下:

int main()
{
	LinkNode* plist = NULL;
	LinkPushFront(&plist, 1);//头插数据
	LinkPushFront(&plist, 2);
	LinkPushFront(&plist, 3);
	LinkPushFront(&plist, 4);
	LinkPushFront(&plist, 5);
	LinkPrint(plist);//打印

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPushBack(&plist, 1);//尾插数据
	LinkPushBack(&plist, 2);
	LinkPushBack(&plist, 3);
	LinkPushBack(&plist, 4);
	LinkPushBack(&plist, 5);
	LinkPrint(plist);

	LinkNode* pos1 = LinkFind(plist, 3);//寻找数据域为3的结点,找到后返回地址
	LinkInsertFront(&plist, pos1, 6);//在3之前插入6
	LinkPrint(plist);

	LinkNode* pos2 = LinkFind(plist, 4);//找到4所在结点的地址
	LinkErase(&plist, pos2);//删除此结点
	LinkPrint(plist);

	LinkDestroy(&plist);//销毁

	return 0;
}

运行结果如图:
运行测试

二、使用头文件——将声明与定义分离

1.头文件Linked-list.h

在链表中,我们为了实现它的基本功能,写了很多的函数,如果我们将结构体,函数的声明,函数的定义,以及函数的调用测试写在同一个源文件中,会大大降低代码的可读性和可维护性;因此,我们可以将结构体以及一些函数的声明放在头文件Linked-list.h中,代码如图:
在这里插入图片描述

2.源文件Linked-list.c

在源文件Linked-list.c中,我们存放函数具体实现的代码,在第一行应加上 #include"Linked-list.h",代码如下:

#include"Linked-list.h"

LinkNode* CreateLinkNode(DataType x)
{
	LinkNode* newnode = (LinkNode*)malloc(sizeof(LinkNode));//开辟新结点,存储数据
	if (newnode == NULL)
	{
		perror("malloc fail");//检查动态内存开辟是否成功
		return NULL;
	}
	newnode->data = x;//给该类型结构体的成员data(数据域)赋值
	newnode->next = NULL;//该结构体的指针域赋值为空指针
	return newnode;//返回是一个指针,与定义函数的返回类型一致
}

void LinkPushBack(LinkNode** pphead, DataType x)
{
	assert(pphead);
	LinkNode* newnode = CreateLinkNode(x);//指针newnode用来接收CreateLinkNode函数创建的新结点
	if (*pphead == NULL)//链表的第一个结点解引用就是空指针
	{
		*pphead = newnode;//赋值开始,刚才的空指针不再为空,链表开始搭建
	}
	else
	{
		LinkNode* tail = *pphead;//找尾,后进入的元素要插在尾部,此处定义一个同型结构体指针变量,负责此任务
		while (tail->next != NULL)//链表最后一个结点,其特点就是指针域为空指针
		{
			tail = tail->next;//依次向后遍历
		}
		tail->next = newnode;//链表最后一个结点的空指针指向了新节点,链接成功
	}
}

void LinkPrint(LinkNode* phead)
{
	LinkNode* cur = phead;//将头指针赋给cur, cur可以自由移动
	while (cur != NULL)//注意循环条件控制
	{
		printf("%d->", cur->data);//指针形式访问结点的数据域
		cur = cur->next;//cur移向下一个结点
	}
	printf("NULL\n");//表示链表最后一个结点指针域为空
}

void LinkPopBack(LinkNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	if ((*pphead)->next == NULL)//对二级指针pphead进行一级解引用,得到指向结点的指针
	{
		free(*pphead);//释放空间
		*pphead = NULL;//将释放了空间的指针置空,防止产生野指针
	}
	else
	{
		LinkNode* tail = *pphead;
		while (tail->next->next != NULL)//tail->next指向tail后面的结点
		{                               //如果tail后面结点的指针域为空,说明后面的结点是链表的尾部
			tail = tail->next;//向后寻找
		}
		free(tail->next);//将最后一个节点的空间释放
		tail->next = NULL;//将释放了空间的指针置空,防止产生野指针
	}
}

void LinkPushFront(LinkNode** pphead, DataType x)
{
	assert(pphead);
	LinkNode* newnode = CreateLinkNode(x);//创建新结点,将其赋给同型结构体指针newnode
	newnode->next = *pphead;//这时新结点的指针域应该指向传入的头部指针*pphead
	*pphead = newnode;//此时头部就是刚插入的新结点newnode,*pphead改变指向,指向链表的头——newnode
}

void LinkPopFront(LinkNode** pphead)
{
	assert(pphead);//首先,头指针的地址不能为空
	assert(*pphead);//其次,头指针本身不能是空指针
	LinkNode* tmp =(*pphead)->next;//临时变量将头结点后面的节点保护起来
	free(*pphead);//这样在释放头指针指向的空间后还能找到后面的结点在哪里
	*pphead = tmp;//最后头指针指向第二个结点
}

LinkNode* LinkFind(LinkNode* phead, DataType x)
{
	LinkNode* cur = phead;//创建一个同型结构体指针变量用于遍历查找
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;//返回的是一个指针,指向要查找的结构体
		}
		cur = cur->next;//cur移向下一个结点
	}
	return NULL;//如果没有找到,返回空指针
}

void LinkInsertFront(LinkNode** pphead, LinkNode* pos, DataType x)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		LinkPushFront(pphead, x);//如果刚好是在首元素前插入,调用之前的头插函数即可
	}
	else
	{
		LinkNode* newnode = CreateLinkNode(x);//插入数据就要创建新结点
		LinkNode* front = *pphead;//创建同型指针变量front,寻找pos之前的那个结点
		while (front->next != pos)//pos前一个结点的特征就是它的指针域(next)指向的是pos
		{
			front = front->next;//向后遍历
		}		
		front->next = newnode;//前一个结点的指针域不再指向pos,而是指向新插入的结点
		newnode->next = pos;//新插入的结点的指针域指向的是pos
	}
}

void LinkErase(LinkNode** pphead, LinkNode* pos)
{
	assert(pphead);
	assert(pos);
	if (*pphead == pos)
	{
		LinkPopFront(pphead);//刚好是第一个结点,调用之前的头部数据删除函数
	}
	else
	{
		LinkNode* front = *pphead;//创建一个同型结构体指针变量,用于寻找pos之前的结点
		while (front->next != pos)
		{
			front = front->next;//向后查找
		}		
		front->next = pos->next;//pos前一个结点的指针域指向pos后一个结点
		free(pos);//释放pos的空间		
	}
}

void LinkDestroy(LinkNode** pphead)
{
	assert(pphead);
	LinkNode* cur = *pphead;
	while (cur != NULL)
	{
		LinkNode* tmp = cur->next;
		free(cur);
		cur = tmp;
	}
	*pphead = NULL;
}

3.源文件Test.c

在源文件Test.c中调用链表的功能函数,完成测试,当然第一行还是得写上 #include"Linked-list.h" 。在这里,我们将上期和这期的内容结合在一起,写了三个测试函数 void Test_one()、void Test_two()和void Test_three(),代码如下:

#include"Linked-list.h"

void Test_one()
{
	LinkNode* plist = NULL;

	LinkPushBack(&plist, 1);//尾插数据
	LinkPushBack(&plist, 2);
	LinkPushBack(&plist, 3);
	LinkPushBack(&plist, 4);
	LinkPushBack(&plist, 5);

	LinkPrint(plist);//打印

	LinkPopBack(&plist);//尾删数据
	LinkPrint(plist);//打印

	LinkPopBack(&plist);//尾删数据
	LinkPrint(plist);//打印

	LinkPopBack(&plist);//尾删数据
	LinkPrint(plist);//打印

	LinkPopBack(&plist);//尾删数据
	LinkPrint(plist);//打印

	LinkPopBack(&plist);//尾删数据
	LinkPrint(plist);//打印
		
	LinkDestroy(&plist);//销毁
}

void Test_two()
{
	LinkNode* plist = NULL;
	LinkPushFront(&plist, 1);//头插数据
	LinkPushFront(&plist, 2);
	LinkPushFront(&plist, 3);
	LinkPushFront(&plist, 4);
	LinkPushFront(&plist, 5);
	LinkPrint(plist);//打印

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkPopFront(&plist);//头删数据
	LinkPrint(plist);

	LinkDestroy(&plist);//销毁
}

void Test_three()
{
	LinkNode* plist = NULL;
	LinkPushBack(&plist, 1);//尾插数据
	LinkPushBack(&plist, 2);
	LinkPushBack(&plist, 3);
	LinkPushBack(&plist, 4);
	LinkPushBack(&plist, 5);
	LinkPrint(plist);

	LinkNode* pos1 = LinkFind(plist, 3);//寻找数据域为3的结点,找到后返回地址
	LinkInsertFront(&plist, pos1, 6);//在3之前插入6
	LinkPrint(plist);

	LinkNode* pos2 = LinkFind(plist, 4);//找到4所在结点的地址
	LinkErase(&plist, pos2);//删除此结点
	LinkPrint(plist);

	LinkDestroy(&plist);//销毁
}

int main()
{
	Test_one();
	Test_two();
	Test_three();

	return 0;
}

三、本期总结+下期预告

本期内容书接上回,完成了单链表的C语言实现,下期内容将为大家带来双向循环链表的内容!

感谢大家的关注,我们下期再见!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值