【数据结构初阶】链表(上)——无头单向非循环链表的实现

目录

一、链表

        1.链表的概念及结构

        2.链表的分类

                <2.1>单向链表和双向链表

                <2.2>带头节点链表和不带头节点链表

               <2.3>循环和非循环链表

二、无头单向非循环链表的实现

        1.创建无头单向非循环链表的节点

        2.为无头单向非循环链表添加数据

                <2.1>在链表尾部插入数据

                <2.2>在链表头部插入数据

                <2.3>在链表pos节点位置之后插入数据

        3.为无头单向非循环链表删除数据

                <3.1>在链表尾部删除数据

                <3.2>在链表头部删除数据 

                <3.3>在链表pos节点之后删除数据

        4.遍历链表

        5.在链表中查找数据

        6.销毁链表

 三、无头单向非循环链表实现完整代码

四、运行测试


经过上期博客的学习我们发现顺序表存在着一些缺陷:

        > 扩容时有一定的代价,尤其是异地扩容时。其次还会造成空间的浪费(空间碎片的产生)。

        > 从头部或中间插入(删除)数据时需要挪动其他数据,效率低下。

那我们是否有一种可以按需申请释放内存、在插入删除数据时不挪动数据的存储方式呢?

链表就很好的解决了以上问题:

一、链表

        1.链表的概念及结构

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

        下面是链表的模拟图:

        我们每次要存储要存储数据时需要向系统空间的堆区申请所存储数据外加一个指针的大小的空间(我们称这样一个大小的空间叫做节(结)点),其中指针空间所存的是下一个节点的地址(如果该节点后无节点该指针指向的地址则为空)。

从上图可看出:

        1.链式结构在逻辑上是连续的,但是在物理上不一定连续。

        2.现实中的结点一般都是从堆上申请出来的。

        3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。

        2.链表的分类

                <2.1>单向链表和双向链表

                <2.2>带头节点链表和不带头节点链表

               <2.3>循环和非循环链表

         虽然链表的种类很多,但是我们最常用的是无头单向非循环链表带头双向循环链表:

         无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

        带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。

下面我们来对无头单向非循环链表来进行实现:

二、无头单向非循环链表的实现

在实现单链表之前我们先要确定节点类型:

typedef int SLTDataType;//数据类型
typedef struct SListNode//节点类型
{
	SLTDataType Data;//数据
	struct SListNode* next;//节点指针
}SListNode;

         :这里的对int类型进行重定义是为了我们更好的看懂Data只是一种数据而不仅仅是int类型。所以我们在使用链表时存储的数据并不限制于int类型,这里仅仅是举例。

        1.创建无头单向非循环链表的节点

SListNode* BuySListNode(SLTDataType x)//x传入要添加的数据
{
	SListNode* p = (SListNode*)malloc(sizeof(SListNode));//为单链表创建一个节点的空间
	if (p == NULL)//判断堆区空间是否申请成功
	{
		perror("malloc");
		exit(-1);
	}
	p->Data = x;//拷贝数据
	p->next = NULL;//由于没有下一个节点,将节点指针置为空
	return p;
}

        2.为无头单向非循环链表添加数据

                <2.1>在链表尾部插入数据

void SListPushBack(SListNode** pplist, SLTDataType x)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
	SListNode* newplist = BuySListNode(x);//创建一个新节点为插入链表做准备
	if (*pplist == NULL)//如果传入的是个空链表就直接插入一个新节点
	{
		*pplist = newplist;
		return;
	}
	//传入的不是空链表就找到链表尾部
	SListNode* tail = *pplist;
	while (tail->next)
	{
		tail = tail->next;
	}
	tail->next = newplist;//在链表尾部插入数据
}

                <2.2>在链表头部插入数据

void SListPushFront(SListNode** pplist, SLTDataType x)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
	SListNode* newplist = BuySListNode(x);//创建一个新节点为插入链表做准备
	newplist->next = *pplist;//将节点插入到链表头部
	*pplist = newplist;//将链表的头指针指向插完节点后的新链表
}

                <2.3>在链表pos节点位置之后插入数据

void SListInsertAfter(SListNode** pos, SLTDataType x)//pos传入要插入其后的节点的位置的地址
{
	assert(*pos);
	SListNode* newplist = BuySListNode(x);//创建一个新节点为插入链表做准备
	newplist->next = (*pos)->next;//将新节点之后指向的节点向插入
	(*pos)->next = newplist;//再将新节点插入pos节点之后的位置
}

        3.为无头单向非循环链表删除数据

                <3.1>在链表尾部删除数据

void SListPopBack(SListNode** pplist)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
	assert(*pplist);//传入的指针不能为空
	if ((*pplist)->next == NULL)//如果此链表只有一个节点,就直接直接释放
	{
		free(*pplist);
		*pplist = NULL;
		return;
	}
	SListNode* tail = *pplist;
	SListNode* temp = *pplist;//记录尾节点前一个节点的位置
	//找到链表尾部
	while (tail->next)
	{
		temp = tail;//记录尾节点前一个节点的位置
		tail = tail->next;
	}
	free(tail);//释放链表尾部节点的空间
	tail = NULL;//将节点置空防止野指针的产生
	temp->next = NULL;//将尾节点前一个节点的指针置空
}

                <3.2>在链表头部删除数据 

void SListPopFront(SListNode** pplist)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
	assert(*pplist);//传入的指针不能为空
	SListNode* Del = *pplist;//记录头节点的地址
	*pplist = (*pplist)->next;//将头节点从链表中销毁
	free(Del);
	Del = NULL;//防止野指针的产生
}

                <3.3>在链表pos节点之后删除数据

void SListEraseAfter(SListNode** pos)//pos传入要删除其后的节点的位置的地址
{
	assert(*pos);//传入的指针不能为空
	SListNode* Del = (*pos)->next;//记录要删除节点的地址
	(*pos)->next = Del->next;//将要删除节点从链表中销毁
	Del->next = NULL;//防止free时释放整个链表空间
	free(Del);
	Del = NULL;//防止野指针的产生
}

        4.遍历链表

void SListPrint(SListNode* plist)
{
	while (plist)//一直遍历到空为止
	{
		printf("%d->", plist->Data);
		plist = plist->next;
	}
	printf("NULL\n");
}

        5.在链表中查找数据

SListNode* SListFind(SListNode* plist, SLTDataType x)
{
	SListNode* find = plist;
	while (find)
	{
		if (find->Data == x)
			return find;//找到返回其地址
		find = find->next;
	}
	return NULL;//没找到返回NULL
}

        6.销毁链表

void SListDestroy(SListNode** plist)
{
	assert(*plist);//传入的指针不能为空
	SListNode* Del = *plist, * temp = *plist;
	while (Del)//释放整个链表
	{
		temp = temp->next;
		free(Del);
		Del = temp;
	}
	*plist = NULL;//置空指针防止野指针的产生
}

 三、无头单向非循环链表实现完整代码

typedef int SLTDataType;//数据类型
typedef struct SListNode//节点类型
{
	SLTDataType Data;//数据
	struct SListNode* next;//节点指针
}SListNode;


//创建一个单链表
SListNode* BuySListNode(SLTDataType x)//x传入要添加的数据
{
	SListNode* p = (SListNode*)malloc(sizeof(SListNode));//为单链表创建一个节点的空间
	if (p == NULL)//判断堆区空间是否申请成功
	{
		perror("malloc");
		exit(-1);
	}
	p->Data = x;//拷贝数据
	p->next = NULL;//由于没有下一个节点,将节点指针置为空
	return p;
}
// 单链表打印
void SListPrint(SListNode* plist)
{
	while (plist)
	{
		printf("%d->", plist->Data);
		plist = plist->next;
	}
	printf("NULL\n");
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
	SListNode* newplist = BuySListNode(x);//创建一个新节点为插入链表做准备
	if (*pplist == NULL)//如果传入的是个空链表就直接插入一个新节点
	{
		*pplist = newplist;
		return;
	}
	//传入的不是空链表就找到链表尾部
	SListNode* tail = *pplist;
	while (tail->next)
	{
		tail = tail->next;
	}
	tail->next = newplist;//在链表尾部插入数据
}
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDataType x)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
	SListNode* newplist = BuySListNode(x);//创建一个新节点为插入链表做准备
	newplist->next = *pplist;//将节点插入到链表头部
	*pplist = newplist;//将链表的头指针指向插完节点后的新链表
}
// 单链表的尾删
void SListPopBack(SListNode** pplist)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
	assert(*pplist);//传入的指针不能为空
	if ((*pplist)->next == NULL)//如果此链表只有一个节点,就直接直接释放
	{
		free(*pplist);
		*pplist = NULL;
		return;
	}
	SListNode* tail = *pplist;
	SListNode* temp = *pplist;//记录尾节点前一个节点的位置
	//找到链表尾部
	while (tail->next)
	{
		temp = tail;//记录尾节点前一个节点的位置
		tail = tail->next;
	}
	free(tail);//释放链表尾部节点的空间
	tail = NULL;//将节点置空防止野指针的产生
	temp->next = NULL;//将尾节点前一个节点的指针置空
}

// 单链表头删
void SListPopFront(SListNode** pplist)//这里由于我们要改变链表指针的指向,需要使用二级指针来控制
{
	assert(*pplist);//传入的指针不能为空
	SListNode* Del = *pplist;//记录头节点的地址
	*pplist = (*pplist)->next;//将头节点从链表中销毁
	free(Del);
	Del = NULL;//防止野指针的产生
}
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
	assert(plist);
	SListNode* find = plist;
	while (find)
	{
		if (find->Data == x)
			return find;//找到返回其地址
		find = find->next;
	}
	return NULL;//没找到返回NULL
}
// 单链表在pos位置之后插入x
void SListInsertAfter(SListNode** pos, SLTDataType x)//pos传入要插入其后的节点的位置
{
	assert(*pos);
	SListNode* newplist = BuySListNode(x);//创建一个新节点为插入链表做准备
	newplist->next = (*pos)->next;//将新节点之后指向的节点向插入
	(*pos)->next = newplist;//再将新节点插入pos节点之后的位置
}
// 单链表删除pos位置之后的值
void SListEraseAfter(SListNode** pos)//pos传入要删除其后的节点的位置
{
	assert(*pos);//传入的指针不能为空
	SListNode* Del = (*pos)->next;//记录要删除节点的地址
	(*pos)->next = Del->next;//将要删除节点从链表中销毁
	Del->next = NULL;//防止free时释放整个链表空间
	free(Del);
	Del = NULL;//防止野指针的产生
}
// 单链表的销毁
void SListDestroy(SListNode** plist)
{
	assert(*plist);//传入的指针不能为空
	SListNode* Del = *plist, * temp = *plist;
	while (Del)//释放整个链表
	{
		temp = temp->next;
		free(Del);
		Del = temp;
	}
	*plist = NULL;//置空指针防止野指针的产生
}

四、运行测试

测试代码:

int main()
{
	SListNode* plist = NULL;
	SListPushBack(&plist, 1);
	SListPrint(plist);
	SListPushBack(&plist, 2);
	SListPrint(plist);
	SListPushBack(&plist, 3);
	SListPrint(plist);
	SListPopBack(&plist);
	SListPrint(plist);
	SListPopFront(&plist);
	SListPrint(plist);
	SListPopBack(&plist);
	SListPrint(plist);
	SListPushBack(&plist, 10);
	SListPrint(plist);
	SListPushFront(&plist, 100);
	SListPrint(plist);
	SListPushBack(&plist, 200);
	SListPrint(plist);
	SListPushBack(&plist, 400);
	SListPrint(plist);
	SListNode* temp = SListFind(plist, 100);
	SListEraseAfter(&temp);
	SListPrint(plist);
	temp = SListFind(plist, 200);
	SListInsertAfter(&temp, 300);
	SListPrint(plist);
	SListDestroy(&plist);
	SListPrint(plist);
	return 0;
}

运行效果:


        好了,本期博客到这里又要结束了,本期博客代码量多难免有不足之处,还请各位大佬们在评论区不吝赐教!

        下期还会对链表进行详细讲解,请各位看官敬请期待~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

1e-12

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

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

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

打赏作者

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

抵扣说明:

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

余额充值