C语言数据结构(3)----无头单向非循环链表

news2024/10/1 3:34:43

目录

1. 链表的概念及结构

2. 链表的分类

3. 无头单向非循环链表的实现(下面称为单链表)

3.1 SListNode* BuySListNode(SLTDateType x) 的实现

 3.2 void SListPrint(SListNode* plist) 的实现

3.3 void SListPushBack(SListNode** pplist, SLTDateType x) 的实现

3.4 void SListPushFront(SListNode** pplist, SLTDateType x) 的实现

3.5 void SListPopBack(SListNode** pplist) 的实现

3.6 void SListPopFront(SListNode** pplist) 的实现

3.7 SListNode* SListFind(SListNode* plist, SLTDateType x) 的实现

3.8 void SListInsertAfter(SListNode* pos, SLTDateType x) 的实现

3.9 void SListEraseAfter(SListNode* pos) 的实现

3.10 void SListDestroy(SListNode* plist) 的实现

4. 题目练习

4.1 链表中的倒数第 K 个节点

4.2 环形链表 Ι && 环形链表 Ⅱ

4.3 反转链表

4.4 合并两个有序链表


1. 链表的概念及结构

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

 链表的逻辑结构其实和上面的通过连接头连接起来的火车车厢差不多。

 注意:

1:从上图中可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续(程序中表现为每个节点在堆上的内存是不连续的)

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

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

2. 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向

 2. 带头或者不带头

 3. 循环和不循环

八种结构是怎么来的呢?需要数学基础哈,这里就不多说了。组合一下!!

虽然说链表有这么多中,但是我们最常用的是两种哈!

1:无头单向非循环链表。

2:带头双向循环链表。

 1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。

3. 无头单向非循环链表的实现(下面称为单链表)

// slist.h
typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;

// 动态申请一个节点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
void SListEraseAfter(SListNode* pos);
// 单链表的销毁
void SListDestroy(SListNode* plist);

3.1 SListNode* BuySListNode(SLTDateType x) 的实现

为啥要将开辟一个新节点封装成函数嘞,因为在单链表的尾插,头插,指定位置后面插入均需要开辟新的节点,如果我们将开辟节点封装成一个函数,就可以少写几行代码,调用函数就阔以啦!

参数列表中的 x 用来初始化向堆区申请的节点。

//动态申请一个节点
SListNode* BuySListNode(SLTDateType x)
{
	//开辟节点
	SListNode* newNode = (SListNode*)malloc(sizeof(SListNode));
	//返回的指针可能为空,判断一下
	if (newNode == NULL)
	{
		perror("BuyNewNode::malloc");
		exit(-1);
	}
	else
	{
		//将值初始化,newNode->next初始化为NULL是很有必要的
		//可以通过后面对该函数的使用来体会。
		newNode->data = x;
		newNode->next = NULL;
	}
}

 3.2 void SListPrint(SListNode* plist) 的实现

参数 plist 是指向第一个结点的指针哈!

这里就有一个小小的问题,在写顺序表的代码时,我们常常对传入的指针进行断言,这里呢到底需不需要断言 plist 指针呢?这里是没有必要的哈,根据顺序表的结构,传入的指针指向的结构体里面存储的是 堆上开辟的数组的指针,如果传入的指针为空,我们尝试去访问顺序表中的数据,就会发生空指针的解引用引起程序崩溃!

 再看链表,一个节点就是一个数据,当 plist 传入空指针时,就说明了此时单链表为空,直接打印一个NULL就行,或者啥都不打印。故不需要断言 plist 指针。

总结:断言可以确保指针的合法使用,如果程序可能出现非法使用指针,则需要断言,不妨称之为暴力检查;或者用 if 进行判断,不妨称之为温柔的检查。不知道 uu 们喜欢哪一种呢?

打印数据就创建一个指针 cur,初始化为 plist,用 cur 指针遍历链表中的数据,直到 cur 为空为止。

//打印单链表
void SListPrint(SListNode* plist)
{
	//初始化cur指针
	SListNode* cur = plist;
	//循环遍历
	while (cur != NULL)
	{
		//生动的显示链表的结构打印一个 ->
		printf("%d->", cur->data);
		cur = cur->next;
	}
	//链表的尾节点指向 NULL
	printf("NULL");
	printf("\n");
}

3.3 void SListPushBack(SListNode** pplist, SLTDateType x) 的实现

这里需要注意的一个点就是为啥传二级指针哈(C++传一级指针加引用也行的哦)。了解了函数栈帧的创建和销毁之后印象最深刻的结论就是这个吧:形参只是实参的临时拷贝,形参的改变不会影响实参。

如果有不理解的地方请参考:http://t.csdn.cn/SYcAp

好的,理解了这个我们就来尝试理解为啥要传二级指针哈。

假设我们传入的是一级指针:当我们的链表为空时,指向第一个节点的指针 plist 就是 NULL,这时我们将 plist 传入 SListPushBack 函数,尾插嘛,需要新节点,调用上面的 BuySListNode 函数即可。既然插入了一个节点,当然是需要改变 plist 的值的。我们直接将该函数返回的新节点的指针赋值给 plist,这能达到改变 plist 的目的吗?

显然是不能的。下图是传入一级指针尾插数据打印后的结果:

这是为啥呢?

 一级指针不行为啥二级指针就行嘞?

 那好,在弄清楚了这个问题,我们就可以得出结论:当我们需要改变 plist 的值时就必须传入plist的地址。

尾插还会分两种情况,当单链表中没有节点时,我们只需要将 newNode 赋值给 *pplist,如果说有节点的话,就需要找到单链表中的尾节点 tail,然后令 tail -> next = newNode即可。还是比较好理解的哈。真正难理解的是二级指针那里呢!!

//单链表的尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newNode = BuySListNode(x);
	//链表中没有节点,直接赋值即可
	if (*pplist == NULL)
	{
		*pplist = newNode;
	}
	else
	{
		//这里是找尾节点
		SListNode* tail = *pplist;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//连接新的节点
		tail->next = newNode;
	}
}

3.4 void SListPushFront(SListNode** pplist, SLTDateType x) 的实现

头插和尾插差不多的哦,头插同样要改变 plist 的值所以也需要传 plist 的地址哦!头插就简单啦:直接让 newNode -> next = *pphead 就行,链表的增删改查函数,你就先想一般的情况,然后嘞在看看有没有特殊的情况,比如说:链表为空,只有一个节点啥的呀,这样就能分析出需不需要把一种情况单独拿出来处理。显然这个头插函数是不需要的哦!

//单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
	assert(pplist);
	SListNode* newNode = BuySListNode(x);

	newNode->next = *pplist;
	*pplist = newNode;
}

3.5 void SListPopBack(SListNode** pplist) 的实现

尾删的话,肯定没有数据的话是不让删的,需要断言 *pplist。同样可能改变 plist 需要传入 plist 的地址哦!

尾删有两种思路的哦!

1:双指针

 根据上面说的链表函数接口的写法,我们把一般的情况写出来了,就得看看有没有特殊的情况捏,显然是有的哦,当链表只有一个节点时,这个程序会崩溃的!当只有一个节点时         (*plplist)->next 就是空,无法进入循环,prev也就是 NULL,prev->next = NULL,这行代码就会发生空指针的解引用,程序会崩溃的!所以需要单独处理只有一个节点的情况。

void SListPopBack(SListNode** pplist)
{
	assert(*pplist);
	assert(pplist);
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SListNode* tail = *pplist;
		SListNode* prev = NULL;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}

2:不找到尾节点,找尾节点的前一个节点

 这里同样是需要单独处理一种情况的喔!

void SListPopBack(SListNode** pplist)
{
	assert(*pplist);
	assert(pplist);
	//只有一个节点的情况
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		//骑士不是找尾节点啦,找的是尾节点的前一个节点
		SListNode* tail = *pplist;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		//释放,置空
		free(tail->next);
		tail->next = NULL;
	}
}

3.6 void SListPopFront(SListNode** pplist) 的实现

头删肯定要改变 plist 的撒,所以要传 plist 的地址哦,同样需要断言,没有数据不允许删除数据。

头删就简单啦,找到 *pplist 的下一个节点,记录下来,然后释放 *pplist,将 *pplist 置为记录下来的那个值就行。

//单链表的头删
void SListPopFront(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);
	//记录*pplist的下一个节点
	SListNode* next = (*pplist)->next;
	//释放,改变原来的plist
	free(*pplist);
	*pplist = next;
}

3.7 SListNode* SListFind(SListNode* plist, SLTDateType x) 的实现

这是在链表中查找值为 x 的第一个节点的,返回这个节点的指针。这个是配合 指定位置删除和插入的函数使用的。

这里不需要改变 plist,不用传 plist 的地址,查找方法就是遍历加判断。

//单链表的查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
	assert(plist);
	//用cur遍历链表
	SListNode* cur = plist;
	while (cur != NULL)
	{
		//查找到值为x的节点返回即可
		if (cur->data == x)
			return cur;
		cur = cur->next;
	}
	//找不到返回NULL
	return NULL;
}

3.8 void SListInsertAfter(SListNode* pos, SLTDateType x) 的实现

这个函数是在 pos 之后插入一个节点。为啥不在 pos 位置之前插入 x 呢,就是麻烦,pos位置之前插入的话,必须遍历找到pos位置之前的结构体,时间复杂度 O(N)。而在pos位置之后的插入,找到pos的下一个节点很轻松,直接插就行了。这也是单链表的缺点,无法向前找元素的哦!

这个 pos 的结构体指针就是通过 SListNode* SListFind(SListNode* plist, SLTDateType x)  的返回值来的哦!

//在pos位置之后插入 x
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
	assert(pos);
	//申请节点
	SListNode* newNode = BuySListNode(x);
	//找到pos的下一个节点
	SListNode* next = pos->next;
	//连接节点
	pos->next = newNode;
	newNode->next = next;
}

3.9 void SListEraseAfter(SListNode* pos) 的实现

这个函数是删除 pos 位置之后的节点。同样不删除 pos 位置 或者 pos位置之前的节点,都是因为找到 pos 位置之前的节点很不容易。单链表没办法!!!

//单链表删除pos位置之后的节点
void SListEraseAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	//记录pos之后的下一个节点
	SListNode* next = pos->next->next;
	free(pos->next);
	pos->next = next;
}

3.10 void SListDestroy(SListNode* plist) 的实现

这个函数简单咯,遍历单链表,一个一个的释放节点就行,注意:一定要在找到下一个节点之后才能释放哦!

//单链表的销毁
void SListDestroy(SListNode* plist)
{
	//用cur遍历链表
	SListNode* cur = plist;
	while (cur != NULL)
	{
		//找到下一个节点
		SListNode* next = cur->next;
		free(cur);
		cur = next;
	}
}

4. 题目练习

4.1 链表中的倒数第 K 个节点

剑指 Offer 22. 链表中倒数第k个节点 - 力扣(LeetCode)

题目详解:

http://t.csdn.cn/NcVND

4.2 环形链表 Ι && 环形链表 Ⅱ

141. 环形链表 - 力扣(LeetCode)

142. 环形链表 II - 力扣(LeetCode)

题目详解:

http://t.csdn.cn/YNwfJ

4.3 反转链表

剑指 Offer 24. 反转链表 - 力扣(LeetCode)

题目详解:

http://t.csdn.cn/kkUvk

4.4 合并两个有序链表

21. 合并两个有序链表 - 力扣(LeetCode)

题目详解:

http://t.csdn.cn/J1LBu

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

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

相关文章

【分布式】分布式场景下的稳定性保障

文章目录1、什么是稳定性保障2、明确稳定性保障目标2.1、明确一级目标2.2、拆解二级目标3、如何进行稳定性保障3.1、全链路梳理3.2、全链路压测3.3、集群扩容3.4、服务限流3.5、提前预案3.6、紧急预案3.7、系统监控4、大促稳定性保障4.1、制定大促计划4.2、大促准备4.3、大促值…

kubeadm方式安装k8s高可用集群(版本1.26x)

K8S官网:https://kubernetes.io/docs/setup/ 高可用Kubernetes集群规划 配置备注系统版本CentOS 7.9Docker版本20.10.xPod网段172.16.0.0/12Service网段10.103.10.0/16 主机IP说明k8s-master01 ~ 03192.168.77.101 ~ 103master节点 * 3k8s-master-lb192.168.77.2…

Tina_Linux配网开发指南

OpenRemoved_Tina_Linux_配网_开发指南 1 概述 1.1 编写目的 介绍Allwinner 平台上基于wifimanager-v2.0 的WiFi 配网方式,包括softap(WiFi ap 模式热点配网),soundwave(声波配网),BLE(蓝牙低功耗配网)。 1.2 适用范围 • allwinner 软件平台tina v5.0 版本及以…

锁相环的组成和原理及应用

一.锁相环的基本组成 许多电子设备要正常工作,通常需要外部的输入信号与内部的振荡信号同步,利用锁相环路就可以实现这个目的。 锁相环路是一种反馈控制电路,简称锁相环(PLL)。锁相环的特点是:利用外部输入的参考信号控制环路内…

Java查漏补缺(04)IDEA安装设置、JDK相关设置、详细设置、工程与模块管理、代码模板的使用、快捷键的使用、DEBUG断点调试、常用插件

Java查漏补缺(04)IDEA安装设置、JDK相关设置、详细设置、工程与模块管理、代码模板的使用、快捷键的使用、DEBUG断点调试、常用插件本章专题与脉络1. 认识IntelliJ IDEA1.1 JetBrains 公司介绍1.2 IntelliJ IDEA 介绍1.3 IDEA的主要优势:(vs …

Ubuntu下不能切换中文,qt creator无法输入中文,sogo输入法(详细步骤)

目录:1、解决ubuntu 不支持切换中文,并安装sogo输入法步骤;2、解决Qt Creator不支持中文输入:详细步骤:一、解决ubuntu 不支持切换中文,并安装sogo输入法步骤:1、如果在键盘输入法系统中&#x…

Scala面向对象

与java的区别和联系 类的定义: class Person{ var name "scala" def sayHello(){ println("Hello,"name) } def getName name } 注意:如果在定义方法的时候指定了(),那么在调用的时候()可写可不写,如果在定…

【并发编程学习篇】深入理解CountDownLatch

一、CountDownLatch介绍 CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值被coun…

【分布式缓存学习篇】Redis数据结构

一、Redis的数据结构 二、String 数据结构 2.1 字符串常用操作 //存入字符串键值对 SET key value //批量存储字符串键值对 MSET key value [key value ...] //存入一个不存在的字符串键值对 SETNX key value //获取一个字符串键值 GET ke…

[C++]继承

🥁作者: 华丞臧 📕​​​​专栏:【C】 各位读者老爷如果觉得博主写的不错,请诸位多多支持(点赞收藏关注)。如果有错误的地方,欢迎在评论区指出。 推荐一款刷题网站 👉LeetCode 文章目录一、继承…

C/C++每日一练(20230227)

目录 1. 按要求排序数组 ★ 2. Z 字形变换 ★★ 3. 下一个排列 ★★ 1. 按要求排序数组 给你一个整数数组 arr 。请你将数组中的元素按照其二进制表示中,数字 1 的数目升序排序。 如果存在多个数字二进制中 1 的数目相同,则必须将它们按照数值大小…

新型智慧城市顶层规划及智慧应用综合解决方案

【版权声明】本资料来源网络,知识分享,仅供个人学习,请勿商用。【侵删致歉】如有侵权请联系小编,将在收到信息后第一时间删除!完整资料领取见文末,部分资料内容: 业务需求分析系统功能需求分析 …

一文了解虚拟人主播

这两年,相信很多人都能在抖音、快手、微视等平台看到虚拟人主播的视频,形象逼真,表情动作自然,语言流畅,乍一看,就是一位真人!那为什么会有这么多人或者企业选择用虚拟人来做主播呢?…

12万字数字政府县级智慧政务云平台建设方案WORD

【版权声明】本资料来源网络,知识分享,仅供个人学习,请勿商用。【侵删致歉】如有侵权请联系小编,将在收到信息后第一时间删除!完整资料领取见文末,部分资料内容: 1.1 总体方案设计 云平台是云计…

评论对软件品牌很重要:有效地生成和管理评论

评论已成为几乎所有产品类别购买过程中的重要组成部分。 客户评论数据告诉我们,92% 的消费者使用在线评论来指导他们的大部分普通购买决策,软件也不例外。B2B 买家在做出购买决定之前会进行大量研究。 为什么评论对软件品牌很重要 B2B 买家特别希望听到已…

对IDEA中断点Suspend 属性理解

suspend的类型分为 1、ALL:有线程进入该断点时,暂停所有线程 2、Thread:有线程进入该断点时,只暂停该线程 讨论下不同线程在同一时间段都遇到断点时,idea的处理方法。假如在执行时间上,thread1会先进入断…

Android实现连线题效果

效果图全部正确:有对有错:结果展示,纯黑色:支持图片:实现思路仔细分析可以发现,连线题的布局可以分为两部分,一个是左右两列矩形,另一个是他们之间的连线。每个矩形的宽高都一样&…

并非从0开始的c++ day8

并非从0开始的c day8结构体结构体嵌套二级指针练习结构体偏移量内存对齐内存对齐的原因如何内存对齐文件操作文件的概念流的概念文本流二进制流文件缓冲区文件打开关闭文件关闭fclose文件读写函数回顾按格式化读写文件文件读写注意事项结构体 结构体嵌套二级指针练习 需求&am…

Delphi 中 FireDAC 数据库连接(定义连接)

一、定义连接(FireDAC)概述连接定义是一组参数,它定义了如何使用特定的FireDAC驱动将一个应用程序连接到DBMS。它相当于一个BDE别名、ADO UDL(存储的OLEDB连接字符串)或ODBC数据源名称(DSN)。关…

Vue下载安装步骤的详细教程(亲测有效) 1

目录 一、【准备工作】nodejs下载安装(npm环境) 1 下载安装nodejs 2 查看环境变量是否添加成功 3、验证是否安装成功 4、修改模块下载位置 (1)查看npm默认存放位置 (2)在 nodejs 安装目录下,创建 “node_global…