【刷题之路Ⅱ】LeetCode 1823. 找出游戏的获胜者(约瑟夫问题)

news2024/12/29 14:09:12

【刷题之路Ⅱ】LeetCode 1823. 找出游戏的获胜者

  • 一、题目描述
  • 二、解题
    • 1、方法1——单向环形链表
      • 1.1、思路分析
      • 1.2、代码实现
    • 2、方法2——队列
      • 2.1、思路分析
      • 2.2、先将队列实现一下
      • 2.3、代码实现

一、题目描述

原题连接: 1823. 找出游戏的获胜者
题目描述:
共有 n 名小伙伴一起做游戏。小伙伴们围成一圈,按 顺时针顺序 从 1 到 n 编号。确切地说,从第 i 名小伙伴顺时针移动一位会到达第 (i+1) 名小伙伴的位置,其中 1 <= i < n ,从第 n 名小伙伴顺时针移动一位会回到第 1 名小伙伴的位置。

游戏遵循如下规则:

从第 1 名小伙伴所在位置 开始 。
沿着顺时针方向数 k 名小伙伴,计数时需要 包含 起始时的那位小伙伴。逐个绕圈进行计数,一些小伙伴可能会被数过不止一次。
你数到的最后一名小伙伴需要离开圈子,并视作输掉游戏。
如果圈子中仍然有不止一名小伙伴,从刚刚输掉的小伙伴的 顺时针下一位 小伙伴 开始,回到步骤 2 继续执行。
否则,圈子中最后一名小伙伴赢得游戏。
给你参与游戏的小伙伴总数 n ,和一个整数 k ,返回游戏的获胜者。

示例 1:

在这里插入图片描述
输入: n = 5, k = 2
输出: 3
解释:游戏运行步骤如下:

  1. 从小伙伴 1 开始。
  2. 顺时针数 2 名小伙伴,也就是小伙伴 1 和 2 。
  3. 小伙伴 2 离开圈子。下一次从小伙伴 3 开始。
  4. 顺时针数 2 名小伙伴,也就是小伙伴 3 和 4 。
  5. 小伙伴 4 离开圈子。下一次从小伙伴 5 开始。
  6. 顺时针数 2 名小伙伴,也就是小伙伴 5 和 1 。
  7. 小伙伴 1 离开圈子。下一次从小伙伴 3 开始。
  8. 顺时针数 2 名小伙伴,也就是小伙伴 3 和 5 。
  9. 小伙伴 5 离开圈子。只剩下小伙伴 3 。所以小伙伴 3 是游戏的获胜者。

示例 2:

输入: n = 6, k = 5
输出: 1
解释:小伙伴离开圈子的顺序:5、4、6、2、3 。小伙伴 1 是游戏的获胜者。

提示:
1 <= k <= n <= 500

进阶:你能否使用线性时间复杂度和常数空间复杂度解决此问题?

二、解题

1、方法1——单向环形链表

1.1、思路分析

正如题目所描述的一样,我们可以让小伙伴们形成一个环,然后从第一个小伙伴开始数k个数,数到第k个小伙伴的时候就将其出队。
非常形象地,我们可以用一个单向循环链表来解决这个问题,起初我们要先创建出一个循环表链:
在这里插入图片描述
当k大于1时,我们用一个cur指针指向第一个节点,然后让cur往后走k - 1步,然后删除一个节点。然后从被删除节点的下一个节点开始在重复上述过程,直到链表中只剩下一个节点,剩下的这一个节点就是胜利者,我们返回他的序号即可。
因为是单链表,所以我们还需要另外一个指针pre,来保存被删除节点的上一个节点,以辅助删除后再连接成环:
在这里插入图片描述
相信这已经是链表的基本操作了,大家应该都能掌握。

而当k等于1时,问题就变得非常简单了,此时的问题就变成依次删除链表中的节点,直到只剩下一个节点。

1.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

// 先定义环形链表节点
typedef struct circleListNode {
    int id;
    struct circleListNode *next;
} cirNode;


int findTheWinner(int n, int k) {
    // 先创建好环形链表
    int i = 0;
    cirNode *head = NULL; // 保存第一个节点
    cirNode *tail = NULL; // 保存最后一个节点
    for (i = 1; i <= n; i++) {
        cirNode *newNode = (cirNode*)malloc(sizeof(cirNode));
        if (NULL == newNode) {
            perror("malloc fail!\n");
            exit(-1);
        }
        newNode->id = i;
        if (1 == i) {
            head = newNode;
            tail = newNode;
        } else {
            tail->next = newNode;
            tail = tail->next;
        }
    }
    // 连接成环
    tail->next = head;
    cirNode *cur = head;
    cirNode *pre = cur; // 保存被删除节点的前一个节点
    while (n > 1) {
        if (k > 1) {
            for (i = 0; i < k - 1; i++) {
            pre = cur;
            cur = cur->next;
            }
            pre->next = cur->next;
            free(cur);
            cur = pre->next;
        } else {
            cirNode *next = cur->next;
            free(cur);
            cur = next;
        }        
        n--;
    }
    int winner = cur->id;
    free(cur);
    return winner;
}

时间复杂度:O(nk),初始时我们要创建单向环形链表,复杂度为O(n)。每一轮我们都要让cur移动k - 1次,重复到链表中只剩1个节点,故需要重复n - 1次,故此操作的复杂为O(nk)。故总的时间复杂度为O(nk)。
空间复杂度:O(n),我们需要n个链表节点连接成环,故空间复杂度为O(n)。

2、方法2——队列

2.1、思路分析

其实我们也可以用一个非循环队列来模拟出一个循环队列,模拟的方法也很简单,我们每次都让队头元素再排到队尾,因为队列的先进先出的规则,这样模拟的结果很好的符合了循环遍历的结果。

所以我们先将小伙伴们全都入队,完事后对头元素就是序号为1的小伙伴。
然后我们从对头元素开始,依次将队头元素移动到队尾,执行k - 1次,然后再将对头元素出队。
重复上述过程,直到队列中只剩一个元素,剩下的就是胜利者,我们返回其序号即可。

2.2、先将队列实现一下

先将队列CV一下:

// 重定义数据类型
typedef int QDataType;
// 定义节点类型
typedef struct QueueNode {
	struct QueueNode* next;
	QDataType data;
} QueueNode;
// 定义队列类型
typedef struct Queue {
	QueueNode* head;
	QueueNode* tail;
} Queue;
// 队列的初始化
void QueueInit(Queue* pq);
// 队列的入队
void QueuePush(Queue* pq, QDataType x);
// 队列的出队
void QueuePop(Queue* pq);
// 返回队列的对头元素
QDataType QueueFront(Queue* pq);
// 返回队列的队尾元素
QDataType QueueBack(Queue* pq);
// 返回队列中的节点个数
int QueueSize(Queue* pq);
// 判断队列是否为空
bool QueueEmpty(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
// 队列的初始化
void QueueInit(Queue* pq) {
	assert(pq);
	pq->head = NULL;
	pq->tail = NULL;
}
// 队列的入队
void QueuePush(Queue* pq, QDataType x) {
	assert(pq);
	// 创建一个新节点
	QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
	if (NULL == newNode) {
		perror("malloc fail!\n");
		exit(-1);
	}
	newNode->data = x;
	if (NULL == pq->head) {
		pq->head = newNode;
		pq->tail = newNode;
		pq->tail->next = NULL;
	}
	else {
		pq->tail->next = newNode;
		pq->tail = pq->tail->next;
		pq->tail->next = NULL;
	}
}
// 队列的出队
void QueuePop(Queue* pq) {
	assert(pq);
	assert(!QueueEmpty(pq));
	QueueNode* next = pq->head->next;
	free(pq->head);
	pq->head = next;
	// 如果对头为空了,我们也要把队尾也给置空,避免野指针
	if (NULL == pq->head) {
		pq->tail = NULL;
	}
}
// 返回队列的对头元素
QDataType QueueFront(Queue* pq) {
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->head->data;
}
// 返回队列的队尾元素
QDataType QueueBack(Queue* pq) {
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->tail->data;
}
// 返回队列中的节点个数
int QueueSize(Queue* pq) {
	assert(pq);
	assert(!QueueEmpty(pq));
	QueueNode* cur = pq->head;
	int size = 0;
	while (cur) {
		size++;
		cur = cur->next;
	}
	return size;
}
// 判断队列是否为空
bool QueueEmpty(Queue* pq) {
	assert(pq);
	return pq->head == NULL;
}
// 销毁队列
void QueueDestroy(Queue* pq) {
	assert(pq);
	assert(!QueueEmpty(pq));
	QueueNode* cur = pq->head;
	QueueNode* next = cur->next;
	while (cur) {
		next = cur->next;
		free(cur);
		cur = next;
	}
	pq->head = NULL;
	pq->tail = NULL;
}

2.3、代码实现

有了以上思路和队列实现,那我们写起代码来也就水到渠成了:

int findTheWinner(int n, int k) {
    int i = 0;
    Queue queue;
    QueueInit(&queue);
    
    // 先将游戏成员全入队
    for (i = 1; i <= n; i++) {
        QueuePush(&queue, i);
    }
    while (QueueSize(&queue) > 1) {
        for (i = 0; i < k - 1; i++) {
            QueuePush(&queue, QueueFront(&queue));
            QueuePop(&queue);
        }
        QueuePop(&queue);
    }
    int winner = QueueFront(&queue);
    QueueDestroy(&queue);
    return winner;
}

时间复杂度:O(nk),此方法和链表的基本一致。
空间复杂度:O(n),队列中最多有n个元素,故空间复杂度为O(n)。

大家可能都直到这一题其实就是我们可能听老师讲过的约瑟夫问题。
记得当初初次接触到这个约瑟夫问题的时候,模模糊糊的搞了一天也还是迷迷糊糊。如今再来看这个问题,就方法1的链表解法我大约用了10分钟不到就写出来了。
所以加大代码练习量是可以做到量变引起质变的!

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

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

相关文章

Java基础篇 | Java基础语法

✅作者简介&#xff1a;大家好&#xff0c;我是Cisyam&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Cisyam-Shark的博客 &#x1f49e;当前专栏&#xff1a; Java从入门到精通 ✨特色…

2023年上半年网络工程师上午真题及答案解析

1.固态硬盘的存储介质是( )。 A.光盘 B.闪存 C.软盘 D.磁盘 2.虚拟存储技术把( )有机地结合起来使用&#xff0c;从而得到一个更大容量的“内存”。 A.内存与外存 B.Cache与内存 C.寄存器与Cache D.Cache与外存 3.下列接口协议中&…

论文阅读 —— 语义激光SLAM

文章目录 点云语义分割算法1 基于点的方法2 基于网格的方法3 基于投影的方法 一、SLOAM1.1 语义部分1.2 SLAM部分1.2.1 树的残差1.2.2 地面的残差1.2.3匹配过程 二、SSC: Semantic Scan Context for Large-Scale Place Recognition2.1 两步全局语义ICP2.1.1 快速偏航角计算2.1.…

【已解决---ChatGPT学术优化下载安装问题集锦】

文章目录 问题1&#xff1a;关于配置完项目后关闭&#xff0c;如何再次打开快速启动。问题2&#xff1a;项目链接打不开,是404。问题3&#xff1a;出现关于API的报错。问题4&#xff1a;[Local Message] Request timeout. Network error. Please check proxy settings in confi…

8.Ansible Variables介绍

什么是Ansible Variables&#xff1f; 就像任何其他脚本或编程语言一样&#xff0c;变量用于存储变化的值&#xff61;例如, 假设我们要尝试执行相同的操作, 将修补程序应用于数百台服务器&#xff61;我们只需要一个playbook就可以满足所有100台服务器的需求&#xff61;但是,…

chatgpt赋能python:Python中提取指定元素——一个简单而精细的方法

Python中提取指定元素——一个简单而精细的方法 在网页抓取中&#xff0c;经常需要提取特定元素&#xff0c;例如标题、段落、图片等&#xff0c;以便于后续的数据处理与分析。而Python则是许多工程师在此领域中的首选语言&#xff0c;其灵活的语法和强大的第三方库给爬虫和数…

idea怎么搭建springboot

一般来说&#xff0c;用IDEA创建SpringBoot项目有两种方式。其一是Spring官网提供了一个快速生成SpringBoot项目的网站&#xff0c;可以在官网直接下载后&#xff0c;再导入IDEA中。另外一种是直接用IDEA创建一个SpringBoot项目&#xff0c;一般开发也是用的这种方式进行创建。…

为什么串行接口速率比并行接口快?

串行接口的速率会比并行快&#xff0c;可以从下面四个方面考虑&#xff1a; ①高速串口不需要时钟信号来同步数据流&#xff0c;也就没有时钟周期性的边沿&#xff0c;频谱不会集中&#xff0c;所以噪声干扰少很多。 以PCIE和SATA为例&#xff0c;时钟信息通过8b/10b编码已经集…

正运动技术运动控制器如何快速实现单轴/多轴同步跟随功能?

本文主要介绍如何使用MOVESYNC指令快速实现单轴/多轴同步跟随功能&#xff0c;适用于XYZ&#xff08;R&#xff09;、SCARA、DELTA等常见机械结构&#xff0c;在流水线点胶、流水线产品分拣、流水线产品搬运等场景中广泛应用。 阅读本文&#xff0c;学习同步跟随的原理和实现方…

抖音seo源码系统开发服务商选择

“账号矩阵”是一种账号运营的高阶玩法&#xff0c;指一个运营主体同时开设多个平台多个账号利用品牌联动的形式来实现账号之间的相关引流&#xff0c;以账号组的形式实现企业营销价值最大化。那么运营多个账号&#xff0c;短视频平台内容是核心&#xff0c;势必要招募多个剪辑…

RK平台使用IO指令

简介 RK平台开发过程经常要用到IO指令&#xff0c;主要是用来读写CPU各个模块寄存器的值&#xff0c;从而实现在线调试。 RK平台的SDK默认有包含IO指令的源码&#xff0c;如果执行的时候找不到指令&#xff0c;可能是没有编译进去&#xff0c;找到对应的编译脚本编译进去即可。…

Dream音频芯片开发虚拟环绕声算法概论

1 项目需求 2 开发平台介绍 Dream S.A.S France公司网站&#xff1a;https://www.dream.fr Dream全系列的芯片包含SAM2000 series ICs、SAM3000 series ICs以及SAM5000 series ICs。 SAM5000 series ICs包括 sam5504、sam5704、sam5708、sam5808、sam5716、sam5916。 目前drea…

为什么电源纹波那么大?

某用户在用500MHz带宽的示波器对其开关电源输出5V信号的纹波进行测试时&#xff0c;发现纹波和噪声的峰峰值达到了900多mV&#xff08;如下图所示&#xff09;&#xff0c;而其开关电源标称的纹波的峰峰值<20mv。虽然用户电路板上后级还有LDO对开关电源的这个输出再进行稳压…

出学校干了 5 年外包,已经废了

如果不是女朋友和我提分手&#xff0c;我估计现在还没醒悟 本科大专&#xff0c;17年通过校招进入某软件公司做测试&#xff0c;干了接近5年的功能。 今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01;而我已经…

Spring面向切面编程(AOP)

Spring面向切面编程&#xff08;AOP&#xff09; 概念 AOP&#xff08;Aspect Oriented Programming&#xff09;&#xff0c;即面向切面编程&#xff0c;利用一种称为"横切"的技术&#xff0c;剖开封装的对象内部&#xff0c;并将那些影响了多个类的公共行为封装到…

DICOM笔记-CT图像的边界

常见CT图像在有效范围内都是有效CT值。 对CT值的处理也就仅限于做斜率和截距的线性处理&#xff1b; 可参加常用的DICOM标签信息&#xff1a; DICOM笔记-DICOM常用Tag标签汇总_dicom tag列表_黑山老妖的博客的博客-CSDN博客文件引言MetaInfoGroupElementTag Description中文解…

尚无忧货运物流app系统享集运转运uniapp系统

物流货运app系统 找货源 找车源 查找货源 开通会员 开创性的物流货运管理云系统&#xff0c;将货运环节中的制造商、承运商、司机和收货方链接在同一平台&#xff0c;轻松管理运输。 <template> <diy ref"diy" v-if"isDiy"></diy&…

vue——antd+elementUi——table表格实现滚动加载(分页滚动加载)——技能提升

今天遇到一个需求&#xff0c;就是要实现表格的滚动加载。 通常我们经常实现的效果是&#xff1a;下图中带分页的表格 如果要实现滚动分页加载的话&#xff0c;则需要保证的一点就是数据量不能过大&#xff0c;过多的数据量会导致页面的卡顿。 下面来介绍滚动分页加载的实现…

jmeter的使用

一、jmeter介绍和下载 1.1 jmeter介绍 Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试&#xff0c;它最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。 JMeter 可以用于对服务器、网络或对象模拟巨大的负载&#xff0c;来自不…

【Python】FastAPI 配置日志即 logging 模块使用

目录 1. 日志 2. FastAPI 示例 1. 日志 日志是对软件执行时所发生事件的一种追踪方式。软件开发人员对他们的代码添加日志调用&#xff0c;借此来指示某事件的发生。一个事件通过一些包含变量数据的描述信息来描述&#xff08;比如&#xff1a;每个事件发生时的数据都是不同的…