指针穿梭,数据流转:探秘C语言实现单向不带头不循环链表

news2024/10/6 2:47:14

在这里插入图片描述

本篇博客会讲解链表的最简单的一种结构:单向+不带头+不循环链表,并使用C语言实现。

概述

链表是一种线性的数据结构,而本篇博客讲解的是链表中最简单的一种结构,它的一个结点的声明如下:

// 单链表存储的数据类型
typedef int SLTDateType;
// 单向+不带头+不循环链表
typedef struct SListNode
{
	SLTDateType data;       // 数据
	struct SListNode* next; // 指向下一个结点
}SListNode;

一个Node中包含2个部分:data用来存储数据,被称作“数据域”;next用来存储一个后继指针,这个指针指向了下一个结点,被称作“指针域”。链表都是由数据域和指针域组成的。

单向不带头不循环链表的特点是:

  1. 只有一个方向,即每个结点内只存储一个后继指针,指向下一个结点,而没有前驱指针。所以,当我们拿到一个结点后,只能顺着往后找,而不能往前找。
  2. 没有哨兵位的头结点,也就是说,所有的结点,包括第一个结点(即头结点),存储的都是有效的数据。
  3. 最后一个结点指向NULL,而不是指向头结点,没有循环。

大概的样子是:
在这里插入图片描述
下面我们用C语言来实现一个单链表出来。

申请结点

首先实现一个函数,来动态的申请一个结点,函数的声明如下:

SListNode* BuySListNode(SLTDateType x);

只需要动态开辟一个结点,并且把数据域初始化为x,指针域初始化为NULL,即可。

SListNode* BuySListNode(SLTDateType x)
{
	// 创建一个结点
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	// 检查是否创建成功
	if (newnode == NULL)
	{
		// 创建失败
		perror("malloc fail");
		return NULL;
	}
	// 创建成功
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

打印

为了方便测试,写一个函数来打印单链表,函数的声明如下:

void SListPrint(SListNode* plist);

只要给我一个头结点,我就能顺着这个头结点,挨个挨个往后打印。如何打印下一个结点的数据呢?每一个结点里的next指针都指向了下一个数据,所以cur=cur->next就能找到cur的下一个结点。

需不需要检查plist是否为NULL呢?不需要!因为当plist为NULL时,代表链表为空,链表为空是可以打印的。就相当于,我银行卡里没钱,你还不给我查询了?

void SListPrint(SListNode* plist)
{
	// 遍历链表,并打印数据
	SListNode* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

尾插

接下来实现一个函数,在链表的尾部插入数据,函数的声明如下:

void SListPushBack(SListNode** pplist, SLTDateType x);

哦,你可能会很奇怪,为什么参数是一个二级指针呢?这是因为,我们规定了,如果一个单链表为空,则对应的头结点的指针也为NULL。那么,当我们对一个空链表尾插时,就需要开辟一个新结点,并且把结点的地址给头结点指针。但是,如果传的是一级指针,比如SListNode* plist,根据“函数传参的本质是拷贝”,plist只是头结点指针的一份临时拷贝,改变plist并不能改变头结点指针。如果我们想改变头结点指针,就得传它的地址,也就是一个二级指针SListNode** pplist

尾插需要分类讨论:

  1. 链表为空,开辟一个新结点,把它的地址给头结点指针即可。
  2. 链表不为空,需要从头结点开始,挨个挨个向后找,找到尾结点(后继指针为NULL的结点),并把尾结点的next改成新的结点的地址,相当于在尾结点后面链接了一个新的结点。

函数内部还需要检查pplist是否是有效的指针。这是因为,哪怕链表是空,头结点指针是NULL,但头结点指针的地址是不可能为NULL的。后面的其他函数如果还涉及到二级指针,也同理需要检查有效性。

那要不要检查*pplist呢?不需要!*pplist代表头指针,当头指针为NULL时,表示链表为空,是可以尾插的。这就相当于,我银行卡里没钱,还不给我存钱了?

void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);

	// 开辟新节点
	SListNode* newnode = BuySListNode(x);

	// 判断链表是否为空
	if (*pplist == NULL)
	{
		// 链表为空
		*pplist = newnode;
	}
	else
	{
		// 链表不为空
		// 找尾结点
		SListNode* tail = *pplist;
		while (tail->next)
		{
			tail = tail->next;
		}

		// 在尾结点后面链接新结点
		tail->next = newnode;
	}
}

头插

接下来实现一个函数,再链表的头部插入数据。函数的声明如下:

void SListPushFront(SListNode** pplist, SLTDateType x);

函数的参数又有一个二级指针,这就非常有意思了。由于头插时,是一定要改变头指针的,而在函数内部改变头指针是要传头指针的地址的,即二级指针。

头插需不需要分类讨论呢?事实上,是不需要的,因为不同情况的处理方式是相同的。

  1. 若链表为空,则直接把头指针改成新的结点的地址。
  2. 若链表非空,则让新结点的next存储原来的头结点的地址,再让头指针指向新的结点。

其实,case 1也能按照case 2一样处理。因为,新结点的next存储的也是NULL,即原头结点的地址,而我们规定了若链表为空,则头指针为NULL。

函数一定要检查pplist的有效性,因为当链表为空时,头指针为NULL,但是头指针的地址不为NULL。

那要不要检查*pplist呢?不需要!*pplist代表头指针,头指针为NULL,代表链表为空,也是可以头插的。还是那句话,我银行卡里没钱,还不给我存钱了?

void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);

	// 开辟新结点
	SListNode* newnode = BuySListNode(x);
	// 链接
	newnode->next = *pplist;
	// 更新头结点
	*pplist = newnode;
}

尾删

下面实现一个函数,来删除链表尾部的数据,函数的声明如下:

void SListPopBack(SListNode** pplist);

emmm,又是二级指针。大家不用意外,单链表的结构存在一定的缺陷,实现起来确实会稍显复杂。那么这次为什么又要使用二级指针呢?因为存在一种情况,当链表只剩下一个结点的时候,尾删后就空了,此时头结点发生了改变,而要在函数内部改变头指针是需要传二级指针的。

尾删可以说是相当的复杂了,要分3种情况来讨论:

  1. 若链表为空,就不能尾删。
  2. 若链表只有1个结点,删完后就空了,需要改变头指针。
  3. 若链表至少有2个结点(包含2个),则需要找到尾结点并且释放掉,还要把尾结点的前一个结点的next置NULL。

这里重点说明第3种情况。我们需要找到尾结点的前一个,所以可以多定义一个tailPrev指针,在tail往后遍历之前,先把tail赋值给tailPrev,tail再往后走,这样tailPrev永远比tail慢一步,当tail走到尾结点时,tailPrev刚好指向尾结点的前一个。

pplist还是需要检查的,这里就不赘述了。

*pplist要不要检查呢?这次就需要检查了!因为*pplist代表头指针,当头指针为NULL时,表示链表为空,链表为空是不能尾删的!也就是说,我银行卡里没钱,就不能再取钱了,否则就乱套了(不考虑信用卡等情况)。

void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	// 防止是空链表
	assert(*pplist);

	// 判断结点数是否多于一个
	if ((*pplist)->next == NULL)
	{
		// 只有一个结点了
		// 释放空间
		free(*pplist);
		// 把链表更新为空链表
		*pplist = NULL;
	}
	else
	{
		// 至少有2个结点
		// 找到尾结点和尾结点前面的那个结点
		SListNode* tail = *pplist; // 尾结点
		SListNode* tailPrev = NULL; // 尾结点前面的那个结点
		while (tail->next)
		{
			// tailPrev比tail慢一步
			tailPrev = tail;
			tail = tail->next;
		}

		// 释放尾结点
		free(tail);
		tail = NULL;
		// tailPrev成为新的尾结点
		tailPrev->next = NULL;
	}
}

头删

接着实现一个函数,删除链表头部的结点,函数声明如下:

void SListPopFront(SListNode** pplist);

我们又见面了,二级指针。这次为什么又要用二级呢?当我们删除头部的结点时,头指针应该指向下一个结点,此时需要再函数内部改变头指针,自然需要传头指针的地址,即二级指针。

和尾删类似,还是需要分3种情况讨论;不过又类似头插,可以把其中2种情况合并一下:

  1. 链表为空,不能头删。
  2. 链表只有1个结点,需要释放掉这个结点,并把头指针置NULL。
  3. 链表至少有2个结点(包括2个),需要释放掉头结点,并让头指针指向原头结点的下一个结点。

显然,case 2和case 3可以合并,因为在case 2中的“把头指针置NULL”,和case 3中的“让头指针指向原头结点的下一个结点”是一回事,因为当只有1个结点时,原头结点的下一个结点是不存在的,此时原头结点中的next指针为NULL。

函数内部仍然要检查pplist,因为pplist是不可能为NULL的;而且要检查*pplist,因为链表为空就不能头删了,这2个检查和尾删的检查类似。

void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	// 保证至少有一个结点
	assert(*pplist);

	// 保存头结点下一个结点或者NULL
	SListNode* next = (*pplist)->next;
	// 释放空间
	free(*pplist);
	// 新的头为next
	*pplist = next;
}

查找

下面我们来实现一个函数,在链表中查找指定数据。函数的声明如下:

SListNode* SListFind(SListNode* plist, SLTDateType x);

这个函数相当于让大家中场休息一下。遍历链表并一一比对即可。

SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	// 遍历数组,查找x
	SListNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			// 找到了
			return cur;
		}
		cur = cur->next;
	}
	// 找不到
	return NULL;
}

后插

接下来实现一个函数,在指定的结点后面插入一个新的结点。函数的声明如下:

void SListInsertAfter(SListNode* pos, SLTDateType x);

我们需要先记录pos的下一个结点next,在pos和next中间插入新的结点即可。

注意检查pos指针的有效性。

void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);

	// 创建新的结点
	SListNode* newnode = BuySListNode(x);
	// 插入到pos的后面
	// 链接newnode和pos->next
	newnode->next = pos->next;
	// 链接pos和newnode
	pos->next = newnode;
}

后删

再来实现一个函数,删除指定结点的后一个结点,函数的声明如下:

void SListEraseAfter(SListNode* pos);

只需要先保存要删除的结点del和要删除的结点的后一个结点next,删除del,并且链接pos和next。

注意检查pos指针的有效性,并且pos->next也不能为NULL,不然的话删啥删。

void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	// 保证后面有结点
	assert(pos->next);

	// 保存pos后面的结点
	SListNode* del = pos->next;
	// 链接pos和del->next
	pos->next = del->next;
	// 释放空间
	free(del);
	del = NULL;
}

插入

接下来实现一个函数,在指定结点的前面插入新的结点。函数的声明如下:

void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x);

好家伙,二级指针又来了。确实,这玩意挺烦人,但不能没有,因为万一是头插,是一定要改变头指针的。

可以分2种情况讨论:

  1. *pplist==pos,则复用头插函数。
  2. 否则,找到pos的前一个结点prev,在prev和pos中间插入新结点即可。

函数需要检查pplist和pos,*pplist就没必要检查了,因为如果头指针为NULL,pos也必然为NULL。

void SListInsert(SListNode** pplist, SListNode* pos, SLTDateType x)
{
	assert(pplist);
	assert(pos);

	// 判断是不是头插
	if (*pplist == pos)
	{
		// 头插
		SListPushFront(pplist, x);
	}
	else
	{
		// 不是头插,即pos前面至少有一个结点
		// 找pos前面的结点
		SListNode* prev = *pplist;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		// 开辟新结点
		SListNode* newnode = BuySListNode(x);
		// 在prev和pos中间插入newnode
		// 链接newnode和pos
		newnode->next = pos;
		// 链接prev和newnode
		prev->next = newnode;
	}
}

删除

下面我们来实现一个函数,删除指定的结点,函数的声明如下:

void SListErase(SListNode** pplist, SListNode* pos);

大家又看到二级指针应该不意外了吧,因为这次可能涉及到头删,分类讨论:

  1. *pplist==pos,则头删。
  2. 否则,找到pos的前一个结点prev和后一个结点next,删除pos,链接prev和next。

同理还是检查pplist和pos,*pplist就没必要检查了,因为若头指针为NULL,pos也一定为NULL。

void SListErase(SListNode** pplist, SListNode* pos)
{
	assert(pplist);
	assert(pos);

	// 判断是不是头删
	if (*pplist == pos)
	{
		// 头删
		SListPopFront(pplist);
	}
	else
	{
		// 不是头删
		// pos前面至少还有一个结点
		// 找pos前面的结点
		SListNode* prev = *pplist;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		// 链接prev和pos->next
		prev->next = pos->next;
		// 删除pos
		free(pos);
		pos = NULL;
	}
}

销毁

有始有终,我们还需要销毁链表,函数声明如下:

void SListDestroy(SListNode** pplist);

依次遍历并且删除即可。注意删除前要保存next,否则就找不到下一个了。

注意检查pplist的有效性,但是不用检查*pplist,因为链表为空也是可以销毁的。

void SListDestroy(SListNode** pplist)
{
	assert(pplist);

	// 遍历链表,并销毁结点
	SListNode* cur = *pplist;
	while (cur)
	{
		// 保存下一个
		SListNode* next = cur->next;
		// 释放空间
		free(cur);
		// 迭代
		cur = next;
	}

	*pplist = NULL;
}

总结

我们实现的链表结构有3个特点:单向+不带头+不循环。这个结构是有缺陷的,具体体现为:尾插、尾删效率较低,因为需要先找尾结点。但是反过来,头插、头删效率非常高,所以在需要大量头插、头删,并且不需要尾插、尾删的场景中,非常适合使用这种链表。

如果你认为链表就这样了,那就大错特错了。链表中有一种王者结构:双向+带头+循环链表,这种链表非常强大,至于具体有多强大,又应该如何实现,欲知后事如何,且听下回分解。

感谢大家的阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/531804.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Dcat Admin文件上传漏洞复现

Dcat Admin框架 Dcat Admin是一个基于laravel-admin二次开发而成的后台系统构建工具,只需极少的代码即可快速构建出一个功能完善的高颜值后台系统。支持页面一键生成CURD代码,内置丰富的后台常用组件,开箱即用,让开发者告别冗杂的…

060基于深度学习的建筑物房屋检测

视频演示和demo仓库地址找060期: 银色子弹zg的个人空间-银色子弹zg个人主页-哔哩哔哩视频 效果图如下: 代码所有文件: 运行01create_txt.py会将data文件下的图片路径及标签保存在txt文本内, 运行02train.py会对图片进行读取并训练模型保存在runs文件…

训练自己的ChatGPT(ChatGLM微调 )

目录 准备 操作 上传数据数据 训练进度 推理 验证 异常处理 总结 参考资料 ChatGLM微调 ptuning 准备 接上文https://blog.csdn.net/dingsai88/article/details/130639365 部署好ChatGLM以后,对它进行微调 操作 如果已经使用过 API 或者web模式的应该已经…

Linux安装elasticsearch、ik分词器、kibana

这里写目录标题 前言下载IK分词器下载Elasticsearch下载Kibana下载JDK安装JDK安装Elasticsearch与IK分词器安装Kibana错误调试参考链接扩展部分 前言 一个PHP程序员接入Elasticsearch并不是公司项目的需求,而是自己平时积累了很多项目信息、代码片段、解决问题的网…

设计模式之【模板方法模式】,模板方法和函数式回调,哪个才是趋势?

文章目录 一、什么是模板方法模式1、主要角色2、应用场景3、优缺点4、注意事项及细节 二、实例1、炒菜案例(1)模板方法模式的钩子方法 2、重构JDBC案例 三、模板方法模式与Callback回调模式1、回调基本原理2、案例一:回调方式重构JDBC3、案例…

Camtasia Studio2023最新版喀秋莎电脑录制屏幕编辑器

不管是在我们平日的工作当中,还是生活当中,camtasia studio可以方便地进行屏幕操作的录制和配音、视频的剪辑和过场动画、添加说明字幕和水印、制作视频封面和菜单、视频压缩和播放。 你都会因为一些事情,从而需要进行录屏的需求。而Camtasi…

超详细,unity如何制作人物行走的遥杆?

介绍 在游戏中,移动遥杆是一种常见的用户界面元素,它允许玩家通过触摸或鼠标输入来控制游戏对象的移动。移动遥杆通常由一个圆形或方形的背景和一个可以拖动的小球(称为拇指杆)组成。玩家可以通过拖动拇指杆来控制游戏对象的移动…

某IC交易网 js逆向解析学习【2023/05/16】

文章目录 文章目录 文章目录前言网址目标参数确认加密点cookie解密第一步hex1算法解析rind和rnns完结撒花前言 可以关注我哟,一起学习,主页有更多练习例子 如果哪个练习我没有写清楚,可以留言我会补充 如果有加密的网站可以留言发给我,一起学习共享学习路程 如侵权,联系我…

Vue.js表单输入绑定

对于Vue来说,使用v-bind并不能解决表单域对象双向绑定的需求。所谓双向绑定,就是无论是通过input还是通过Vue对象,都能修改绑定的数据对象的值。Vue提供了v-model进行双向绑定。本章将重点讲解表单域对象的双向绑定方法和技巧。 10.1 实现双…

单片机的介绍

目录 一、介绍 1.单片机简介 2.单片机型号 3.体系 二、硬件基础 1.引言 2.电路基础 电的类比 电流 电压 电路 3.电子元器件 电阻 电容 二极管 三极管 4.常见电气接口 传统音频 视频 电源 RJ45网口 DB9串口 5.开发板/最小系统板 三、STM32介绍 1.简介…

JAVA电商 B2B2C商城系统 多用户商城系统 直播带货 新零售商城 o2o商城 电子商务 拼团商城 分销商城

JAVA电商 B2B2C商城系统 多用户商城系统 直播带货 新零售商城 o2o商城 电子商务 拼团商城 分销商城 1. 鸿鹄Cloud架构清单 2. Commonservice(通用服务) 通用服务:对spring Cloud组件的使用&封装,是一套完整的针对于分布式微…

Android Studio中的布局讲解

文章目录 1.LinearLayout(线性布局)2.RelativeLayout(相对布局)相对于兄弟元素:相对于父元素对齐方式间隔 3.GridLayout(网格布局)设置最大列数设置最大行数指定控件的位置 4.FrameLayout&#…

包管理工具:pnpm | 京东云技术团队

作者:京东零售 杨秀竹 pnpm 是什么 pnpm( performant npm )指的是高性能的 npm,与 npm 和 yarn 一样是一款包管理工具,其根据自身独特的包管理方法解决了 npm、yarn 内部潜在的安全及性能问题,在多数情况…

耗子叔-我的互联网引路人

早上一早看到各大程序员群提到左耳朵耗子-陈皓,因为心梗辞世的信息,真的让人难以置信,因为据我所知他还不到50。 虽然我从来没见过他,交谈也很少,但是我知道他的情况,知道他的公司,知道他的好恶…

不要再问我加密的问题了,使用crypto-js中的AES加密方法,连续多次加密/解密,注意事项

每日鸡汤,每个你想要学习的念头,都是未来的你向自己求救 需求:有一段字符串text,有3个key,后端用这三个key一次加密;然后把加密后的字符串返回给前端,前端用这3个key依次解密,得到原…

剖析:在线帮助中心对企业能够起到什么作用?

随着互联网技术的不断发展和普及,越来越多的企业开始将自己的业务转移到了线上。这种转移不仅能够大幅度提高企业的效率,还能够让企业的服务更加贴近用户的需求。然而,在线服务也存在着一些问题,比如用户可能会遇到一些困难&#…

大人,时代变了!缺少成本票可不能买发票啊,是有办法的!

业务是流程,财税是结果,税收问题千千万,关注《税算盘》来帮你找答案。 企业所得税和增值税一样,都是我国重要的税收之一。企业所得税征收对象为企业的利润部分,再度细分就与企业的成本票有关。 企业所得税高是如今众…

电商系统分类树查询功能优化方案总结

前言 分类树查询功能,在各个业务系统中可以说随处可见,特别是在电商系统中。 但就是这样一个简单的分类树查询功能,我们却优化了5次。 到底是怎么回事呢? 背景 我们的网站使用了SpringBoot推荐的模板引擎:Thymelea…

案例5:Java大学生创新创业项目管理设计与实现任务书

博主介绍:✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专…

RK3568|3588|3566处理器属于什么档次?

随着科技的迅猛发展,处理器作为计算机和电子设备的核心组件,其性能的提升对于设备的功能和用户体验起着至关重要的作用。在处理器市场中,不同的处理器被划分为不同的档次,以便用户能够更好地选择适合自己需求的产品。那么&#xf…