C语言中的数据结构--链表的应用2(3)

news2024/11/15 11:09:31

前言

上一节我们学习了链表的应用,那么这一节我们继续加深一下对链表的理解,我们继续通过Leetcode的经典题目来了解一下链表在实际应用中的功能,废话不多说,我们正式进入今天的学习

单链表相关经典算法OJ题4:合并两个有序链表

https://leetcode.cn/problems/merge-two-sorted-lists/description/

题目详情

该题目与顺序表中的合并两个有序的数组的题目比较类似

题解

我们需要先创建一个新链表,用newHead和newTail分别指向链表的头和尾,再定义两个指针,l1指针用于第一个链表,l2指针用于第二个链表;

我们用l1指针指向的节点的数据大小和l2指针指向的节点的数据大小作比较,若

1.l1指向的节点的数据大于l2指向的节点的数据,则把l2指针指向的节点拿下来到一个新的链表尾插,再让l2指针执行++操作

2..l2指向的节点的数据大于l1指向的节点的数据,则把l1指针指向的节点拿下来到一个新的链表尾插,再让l1指针执行++操作

总的来说就是谁小就拿谁

在我们遍历原链表的过程中,会存在两种结果

1.l1为空,l2不为空

2.l2为空,l1不为空

我们在把第一个节点拿到新链表的时候,newHead和newTail都指向这一个节点,在这种情况下,该节点既为头也为尾

在我们继续向后拿入节点的时候,我们让newTail指向当前节点的位置,而newHead指针保持不动

以此类推,继续循环此操作,直到跳出循环,此时l1或者l2中的一个指针必定是走向了NULL指针

因为链表是升序的,我们此时只需要把没有走完的那个链表的剩余元素全部尾插到新链表就完成任务了

因为原链表可以为空。如果两个链表中的某个链表为空的话,可能就会存在对空指针的解引用,所以我们需要考虑空链表的问题;

根据上述我们梳理的条件,我们可以写出代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) 
{
	//判空
	if (list1 == NULL)
	{
		return list2;
	}
	if (list2 == NULL)
	{
		return list1;
	}
	
	ListNode* l1 = list1;
	ListNode* l2 = list2;
	//创建的新链表
	ListNode* newHead, * newTail;
	newHead = newTail = NULL;

	while (l1 && l2)
	{
		if (l1->val < l2->val)
		{
			//尾插l1
			if (newHead == NULL)
			{
				newHead = newTail = l1;
			}
			
			else
			{
				newTail->next = l1;
				newTail = newTail->next;
			}
			l1 = l1->next;
		}
		
		else
		{
			//尾插l2
			if (newHead == NULL)
			{
				newHead = newTail = l2;
			}
			else
			{
				newTail->next = l2;
				newTail = newTail->next;
			}
			l2 = l2->next;
		}
	}
	//跳出循环后有两种情况:要么l1为空,要么l2为空
	if (l2)
	{
		newTail->next = l2;
	}
	if (l1)
	{
		newTail->next = l1;
	}
	return newHead;
}

我们此时在Leetcode的官网运行一下看看是否编写成功:

代码运行没有出现错误,编写成功

优化

我们重新审视一下代码,我们发现存在重复判断,我们每次拿下来节点的时候都需要对判断该节点是不是第一个节点的操作有些麻烦,会拖累程序的运行时间:

if (newHead == NULL)
{
	newHead = newTail = l1;
}

我们有没有什么办法优化一下呢?

这里代码存在重复的原因是因为:新链表是空链表,我们能不能让新链表不是空链表呢?这样就不需要重复的判断了,直接进行尾插就好了

因为刚开始创建新链表的时候,我们把newHead和newTail都设置成了空指针,此时我们可以让它动态申请一个空间,我们在这个新空间里不存储任何的数据

newHead = newTail = (ListNode*)malloc(sizeof(ListNode));

此时的链表就不为空了,头尾指针都指向了一个有效的节点,只不过该节点里面没有存储任何的数据

该节点实际上是链表分类中的一种,叫做带头链表

在两个链表合并结束了以后,我们只需要将newHead的下一个节点返回就行就行

在进行优化后我们的代码如下:

#define _CRT_SECURE_NO_WARNINGS 1

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
typedef struct ListNode ListNode;

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) 
{
	//判空
	if (list1 == NULL)
	{
		return list2;
	}
	if (list2 == NULL)
	{
		return list1;
	}
	
	ListNode* l1 = list1;
	ListNode* l2 = list2;
	//创建的新链表
	ListNode* newHead, * newTail;
	newHead = newTail = (ListNode*)malloc(sizeof(ListNode));

	while (l1 && l2)
	{
		if (l1->val < l2->val)
		{
			//尾插l1
			newTail->next = l1;
			newTail = newTail->next;
			l1 = l1->next;
		}

		else
		{
			//尾插l2
			newTail->next = l2;
			newTail = newTail->next;
			l2 = l2->next;
		}
	}
	//跳出循环后有两种情况:要么l1为空,要么l2为空
	if (l2)
	{
		newTail->next = l2;
	}
	if (l1)
	{
		newTail->next = l1;
	}
	ListNode* ret = newHead->next;
	free(newHead);
	return ret;
}

我们再检验一下代码是否正确:

代码没有错误,优化成功

循环链表经典应用-环形链表的约瑟夫问题

我们首先需要知道循环链表是什么东西

我们知道,链表的最后一个节点指向的是空指针NULL,而循环链表则不同,循环链表的最后一个节点指向的是该链表的第一个节点,这样就让链表成为了一个闭环

故事

著名的Josephus问题:据说著名犹太历史学家 Josephus 有过以下的故事:在罗⻢⼈占领乔塔帕特后,39个犹太⼈与 Josephus及他的朋友躲到⼀个洞中,39个犹太⼈决定宁愿死也不要被⼈抓到,于是决定了⼀个⾃杀⽅式,41个⼈排成⼀个圆圈,由第1个⼈开始报数,每报数到第3⼈该⼈就必须⾃杀,然后再由下⼀ 个重新报数,直到所有⼈都⾃杀⾝亡为⽌。 然⽽Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与⾃⼰安排在 第16个与第31个位置,于是逃过了这场死亡游戏

题目详情

题解

我们首先需要定义两个指针prev和pcur,其中我们把pcur指针放在第一个节点的位置,让pcur指针去遍历链表,我们把prev指针放到循环链表的最后一个节点,prev指针也随着pcur指针的遍历而向后走,prev指针始终在pcur指针的后一位。(设m=5,n=2)

我们在遍历循环链表的时候,若是找到了顺序为n的节点,我们不能直接释放掉这一个节点,因为要是直接释放掉这个节点,那么顺序为n-1的节点就无法再找到顺序为n+1的节点并且连接起来。

当pcur指针找到了第n个节点时,我们让prev->next指向pcur的下一个节点,然后再把pcur释放掉。此时pcur已经变成了一个野指针,我们现在将pcur赋予原pcur的下一个结点的地址

此时我们重新计数,当pcur再次找到顺序为n的节点时,重复执行之前的操作

以此类推,直到原链表里面的节点个数小于n

此时3的next指针指向了它本身

步骤1:创建带环链表

我们需要先创建头节点,然后根据步骤依次向下创建新的节点,我们首先封装一个函数应用于创建节点,该代码在链表专题中提及过,所以不再做过多的讲解:

//创建节点
ListNode* buyNode(int x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	if (node == NULL)
	{
		exit(1);
	}
	node->val = x;
	node->next = NULL;
	return node;
}

我们封装完创建节点的函数以后,紧接着封装一个创建带环链表的函数:

//创建带环链表
ListNode* creatCircle(int n)
{
	//创建第一个节点
	ListNode* phead = buyNode(1);
	ListNode* ptail = phead;
	//向后创建
	for (int i = 2; i <= n; i++)
	{
		ptail->next = buyNode(i);
		ptail = ptail->next;
	}
	//连接为闭环
	ptail->next = phead;
	return ptail;
}

在该函数中,我们需要返回的是尾节点,因为若是n=1,则需要找到头节点的前一个元素;如果返回的是头节点,那么将找不到头节点之前的尾节点;

步骤2:遍历链表并且计数

在创建完带环链表之后我们需要遍历链表并且计数,此时我们需要创建一个count变量,count变量应该初始化为1

当count的取值等于n的时候,我们此时需要销毁pcur节点

在销毁pcur之前,我们需要先把prev和pcur指向的节点的下一个节点连接起来

根据这些推论,我们可以写出代码如下:

int ysf(int n, int m)
{
	//根据n创建带环链表
	ListNode* prev = creatCircle(n);
	ListNode* pcur = prev->next;
	int count = 1;
	while (pcur->next != pcur)
	{
		if (count == m)
		{
			//销毁pcur节点
			prev->next = pcur->next;
			free(pcur);
			pcur = prev->next;
			count = 1;
		}
		else
		{
			//此时不需要销毁节点
			prev = pcur;
			pcur = pcur->next;
			count++;
		}

	}
	return pcur->val;
}

此时我们的任务就完成了,我们看看代码是否正确:

代码成功运行

该题思路上难度并不大。但是我们要注意细节,不然很有可能会出现错误

单链表相关经典算法OJ题5:分割链表

https://leetcode.cn/problems/partition-list-lcci/

题目详情

题解

方案一:(在原链表上进行修改)

我们定义一个指针pcur指向第一个节点,我们从第一个节点开始遍历;

如果我们遍历到小于3的节点,那么该节点就在原地保持不动;

若是遍历到了大于或者等于3的节点,那么我们先将该节点的前一个节点和后一个节点连接起来,再把该节点尾插到链表的末端;

当我们遍历完全链表的时候,我们的任务也就完成了;

因为该方法需要频繁的将节点断开、连接,而且需要使用多个指针(prev、pcur、ptail、phead)所以实现起来有点麻烦,一般不太推荐使用该方法

方案二:(在新链表上进行修改)

我们先定义pcur指针用来遍历原链表

我们动态申请一个哨兵位,并且定义两个变量newHead和newTail,将这两个变量都指向哨兵位;

若是pcur遍历的第一个节点的值小于x,则把该节点插到哨兵位后面,并且将newTail移动到该节点处;

若是pcur遍历的节点的值小于x,则把该节点头插到新链表中去;

若是pcur遍历的节点的值大于x,则把该节点尾插到新链表中去;

因为题目中说明了我们不需要保留每个分区中各个结点的初始相对位置,所以节点与节点之间的位置可以是随意的;

方案三:(小链表和大链表)

我们创建两个新链表,一个链表仅存放比x小的节点,另外一个链表仅存放比x大的节点;

在小链表中,我们创建两个变量lessHead和lessTail表示小链表的头和尾,我们此时动态申请一个哨兵位,里面不存放任何有效的值,若是原链表中的节点小于x,则该节点直接尾插到小链表中,并且让lessTail指向这个新插入的节点;

在大链表中,我们创建两个变量greaterHead和greaterTail表示小链表的头和尾,我们此时动态申请一个哨兵位,里面不存放任何有效的值,若是原链表中的节点大于x,则该节点直接尾插到小链表中,并且让greaterTail指向这个新插入的节点;

我们遍历完全链表时,大链表和小链表都已经全部插入完成了;

因为大小链表可以为空,我们需要进行判空操作,避免存在对空指针的解引用

我们将小链表的尾节点和大链表的第一个有效的节点(不能和大链表的哨兵位连接在一起)首尾相连,这样我们就完成了任务;

首尾相连以后,如果大链表的尾节点不是原链表的尾节点,那么大链表尾节点指向的节点一定是一个小链表之中的节点,在我们代码运行的时候,大链表最后一个节点本来是应该指向空指针的,但是它不是原链表中的尾节点,例如代码示例中的5,它会指向小链表中的2,此时代码就会进入死循环,所以我们要考虑大链表的尾节点的指向是哪里,要手动把大链表尾节点指向NULL

我们再来考虑一下特殊情况:

若是原链表中只有一个数据1,那么大链表里面仅仅只有一个哨兵位,我们同样按步骤的思路去思考,发现仍然满足题意,所以代码不用进行修改

下面我们来试着实现该代码:

typedef struct ListNode ListNode;

struct ListNode* partition(struct ListNode* head, int x) 
{
	//判空
	if (head == NULL)
	{
		return head;
	}
	//创建两个带头链表
	ListNode* lessHead, * lessTail;
	ListNode* greaterHead, * greaterTail;
	lessHead = lessTail = (ListNode*)malloc(sizeof(ListNode));
	greaterHead = greaterTail = (ListNode*)malloc(sizeof(ListNode));
	//遍历原链表,将原链表的节点尾插到大小链表中
	ListNode* pcur = head;
	while (pcur)
	{
		if (pcur->val < x)
		{
			//尾插到小链表
			lessTail->next = pcur;
			lessTail = lessTail->next;
		}
		else
		{
			//尾插到大链表中
			greaterTail->next = pcur;
			greaterTail = greaterTail->next;
		}
		pcur = pcur->next;
	}
	//处理大链表尾节点next指针指向+next指针初始化
	greaterTail->next = NULL;
	//小链表大链表首尾相连
	lessTail->next = greaterHead->next;
	//释放
	ListNode* ret = lessHead->next;
	free(lessHead);
	free(greaterHead);
	lessHead = greaterHead = NULL;
	return ret;
}

我们试着运行一下代码:

代码成功运行,编写成功

结尾

本节我们同样是学习链表的应用,通过这两节的学习,我们对链表的理解更加深入了,那么关于链表的所有内容到了本节为止就暂时告一段落了,下一节我们将学习双向链表,谢谢您的浏览!!!

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

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

相关文章

【前端工程化指南】什么是版本控制系统?

什么是版本控制系统 想必大家在多人开发时一定会遇到这样的问题&#xff1a; 每次集中合并大家的代码都要通过U盘、网盘等各类传输工具集中代码&#xff0c;非常麻烦。在多人同时修改同一文件或相同部分代码时&#xff0c;可能会产生冲突&#xff0c;开发人员需要手动比较代码…

自编译支持CUDA硬解的OPENCV和FFMPEG

1 整体思路 查阅opencv的官方文档&#xff0c;可看到有个cudacodec扩展&#xff0c;用他可方便的进行编解码。唯一麻烦的是需要自行编译opencv。 同时&#xff0c;为了考虑后续方便&#xff0c;顺手编译了FFMPEG&#xff0c;并将其与OPENCV绑定。 在之前的博文“鲲鹏主机昇腾A…

《系统分析与设计》实验-----需求规格说明书 哈尔滨理工大学

文章目录 需求规格说明书1&#xff0e;引言1.1编写目的1.2项目背景1.3定义1.4参考资料 2&#xff0e;任务概述2.1目标2.2运行环境2.3条件与限制 3&#xff0e;数据描述3.1静态数据3.2动态数据3.3数据库介绍3.4数据词典3.5数据采集 4&#xff0e;功能需求4.1功能划分4.2功能描述…

arxiv文章导出的bibtex格式是misc导致latex引用不正确

问题 在arxiv官网上右下角导出bibtex&#xff0c;发现是misc格式&#xff0c;然后我用的是springer的期刊latex模板&#xff0c;发现引用不正确。 引用效果如下&#xff0c;就只有一个2024。 解决方案&#xff1a; 把上面那个bibtex手动改成下面这个。 article{liu2024in…

SpringCloud实用篇(四)——Nacos

Nacos nacos官方网站&#xff1a;https://nacos.io/ nacos是阿里巴巴的产品&#xff0c;现在是springcloud的一个组件&#xff0c;相比于eureka的功能更加丰富&#xff0c;在国内备受欢迎 nacos的安装 下载地址&#xff1a;https://github.com/alibaba/nacos/releases/ 启动…

【寒假集训营总结笔记——7道优质好题】

牛客寒假集训营总结笔记——7道优质好题 一、Trie树的应用&#xff1a; 题目链接&#xff1a;Tokitsukaze and Min-Max XOR 1、题意 2、题解 1、首先这道题的答案和元素本身的顺序是无关的&#xff0c;因为假如你选择了一些数字&#xff0c;它是默认必须排好序才能记作是答案…

docker特殊问题处理3——docker-compose安装配置nacos

最近几年随着大数据和人工智能持续大热&#xff0c;容器化安装部署运维已经走进了各个中小公司&#xff0c;也得已让众多开发者能上手实际操作&#xff0c;不过说真心话&#xff0c;“万物皆可容器化”的理念越来越深入人心。 而如何使用docker-compose安装&#xff0c;配置&a…

mxnet安装

ChatGPT 安装 MXNet 是一个非常直接的过程&#xff0c;可以通过几种方法实现&#xff0c;包括使用Python的包管理工具pip安装预编译的二进制包&#xff0c;或者从源代码编译。以下是使用pip安装MXNet的基本步骤&#xff1a;1. 首先&#xff0c;确保已经安装了Python和pip。通常…

ELK(Elasticsearch+Logstash+Kibana)日志分析系统

目录 前言 一、ELK日志分析系统概述 1、三大组件工具介绍 1.1 Elasticsearch 1.1.1 Elasticsearch概念 1.1.2 关系型数据库和ElasticSearch中的对应关系 1.1.3 Elasticsearch提供的操作命令 1.2 Logstash 1.2.1 Logstash概念 1.2.2 Logstash的主要组件 1.2.3 Logsta…

Weblogic任意文件上传漏洞(CVE-2018-2894)漏洞复现(基于vulhub)

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java、PHP】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收…

【面试八股总结】排序算法(一)

参考资料 &#xff1a;阿秀 一、冒泡排序 冒泡排序就是把小的元素往前交换或者把大的元素往后交换&#xff0c;比较相邻的两个元素&#xff0c;交换也发生在这两个元素之间。具体步骤&#xff1a; 比较相邻的元素。如果第一个比第二个大&#xff0c;就交换他们两个。对每一对…

RabbitMQ实战教程(1)

RabbitMQ 一、RabbitMQ介绍 1.1 现存问题 服务调用&#xff1a;两个服务调用时&#xff0c;我们可以通过传统的HTTP方式&#xff0c;让服务A直接去调用服务B的接口&#xff0c;但是这种方式是同步的方式&#xff0c;虽然可以采用SpringBoot提供的Async注解实现异步调用&…

CSS导读 (元素显示模式 上)

&#xff08;大家好&#xff0c;今天我们将继续来学习CSS的相关知识&#xff0c;大家可以在评论区进行互动答疑哦~加油&#xff01;&#x1f495;&#xff09; 目录 三、CSS的元素显示模式 3.1 什么是元素显示模式 3.2 块元素 3.3 行内元素 3.4 行内块元素 3.5 元素…

Spring Boot中整合JodConverter实现文件在线预览

Spring Boot中整合JodConverter实现文件在线预览 1.安装LibreOffice 24.2 下载地址 LibreOffice 是一款功能强大的办公软件&#xff0c;默认使用开放文档格式 (OpenDocument Format , ODF), 并支持 *.docx, *.xlsx, *.pptx 等其他格式。 它包含了 Writer, Calc, Impress, Dra…

【学习资源】自适应学习的理论、典型产品和参考代码

图片来源&#xff1a;https://www.evelynlearning.com/adaptive-learning-in-the-classroom/ 自适应学习的类别 自适应学习目前分三个层次&#xff0c;包括学习活动层次、题目层次和知识点层次的自适应。以下分别从理论、典型产品和参考代码介绍三个层次。 学习活动层次的自适…

季节更迭 关爱不变 | 鲁南制药四季守护您的健康生活

春天&#xff0c;万物复苏的季节&#xff0c;一切都充满了生机和活力。在春日的阳光下&#xff0c;鲜花盛开&#xff0c;绿叶茂盛&#xff0c;鸟儿欢歌&#xff0c;蝴蝶翩翩起舞。我们的身体也需要特别的关爱和养护&#xff0c;保持健康和活力&#xff0c;更好地迎接每一次季节…

Shiro——01,环境搭建

环境搭建 一、什么是 Shiro&#xff1a;二、Shir 核心组件三、Shiro 运行机制如图四、用户角色权限三者关系五、搭建环境一键三连有没有捏~~ 一、什么是 Shiro&#xff1a; 官网&#xff1a;http://shiro.apache.org/ 是一款主流的 Java 安全框架&#xff0c;不依赖任何容器&…

ssm+springboot校园勤工俭学助学兼职系统

本校园勤工俭学兼职系统主要包括三大功能模块&#xff0c;即管理员功能模块和学生功能模块及企业功能模块。 &#xff08;1&#xff09;管理员模块&#xff1a;系统中的核心用户是管理员&#xff0c;管理员登录后&#xff0c;通过管理员功能来管理后台系统。主要功能有&#xf…

LlamaIndex 文档 2

文章目录 一、构建 LLM 应用构建LLM 应用的关键步骤 二、使用LLM可用的LLM使用本地LLM Prompts 三、加载数据&#xff08;提取&#xff09;Loaders1、使用 SimpleDirectoryReader 加载2、使用 LlamaHub 的 Readers3、直接创建文档 转换 Transformations1、高级转换 API2、较低级…

React-State状态

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;React篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来React篇专栏内容:React-State状态 如果将state与vue中的某个点做类比的话&#xff0c;则其相当于vue组件中的dat…