本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
前几天在leecode的每日一题做到了一道消除游戏,除了使用了比较通用的解法还顺带学习了约瑟夫环。
题目描述
今天的题目为leetcode上的 390. 消除游戏 难度为 中等
列表 arr 由在范围 [1, n] 中的所有整数组成,并按严格递增排序。请你对 arr 应用下述算法:
- 从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾。
- 重复上面的步骤,但这次是从右到左。也就是,删除最右侧的数字,然后剩下的数字每隔一个删除一个。
- 不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。
给你整数 n ,返回 arr 最后剩下的数字。
示例 1:
输入:n = 9
输出:6
解释:
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
arr = [2, 4, 6, 8]
arr = [2, 6]
arr = [6]
示例 2:
输入:n = 1
输出:1
提示:
- 1 <= n <= 10^9
第一种题解
通过找规律的方法去找到每次执行后数组变化的规律
分析
首先我们看到题要去分析,题目中最重要的几个关键点是什么。
-
lastElement为数组最后剩下的元素
每一种情况的最后都是要剩下一个元素,所以代码中一定要有一个标记标记着最后剩下来的那个元素, 这里我们定义为 lastElement 作为最后剩下的元素,它的初始值为1
-
定义公差为step
每删除一轮之后,数组的公差就会加倍,拿上方的示例一来做示范,第一行数组公差为1,第二行为2, 第三行为4...以此类推,这样我们就能够得到公差的值的公式:
step[i+1] = step[i]*2 -
定义元素个数为n
最开始传入n个元素,每轮过后删除一半的元素,因为要考虑元素个数的奇偶问题,在奇数个的时候会多删除一个,所以n每轮除以2然后向下取整,能够得到剩余元素个数的公式:
n[i+1] = -
判断删除方向的参数 left
最后我们还要知道每一次循环的时候,是从左往右还是从右往左,这里可以定义一个参数来判断,也可以不定义参数,直接通过定义一个循环轮次也就是i来判断现在是哪个方向。
示例
首先我们要知道从左和从右开始两种情况有什么不同:
- 从左往右的情况因为第一个元素一定会被删除,所以 lastElement 每次都加上公差。
- 从右往左的情况第一个元素是否被删除就要看数组的个数,奇数则被删除,偶数则不会,要做一个判断,如果被删除了依然是加上公差。
接下去我们拿一个 arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 十个元素的情况来做一个示例, 初始的 lastElement = 1, step = 1, n = 10, left = true
第一轮从左向右删除 arr = [2, 4, 6, 8, 10], lastElement = 2, step = 2, n = 5, left = false
第二轮从右向左删除 arr = [4, 8], lastElement = 4, step = 4, n = 2, left = true
第三轮从左向右删除 arr = [8], lastElement = 8, step = 8, n = 1, left = false
这样就能找到我们需要的元素,无论数组多大只要一直重复这个过程即可,接下来上代码。
/**
* @param {number} n
* @return {number}
*/
var lastRemaining = function(n) {
let lastElement = 1;
let step = 1;
let left = true;
while (n > 1) {
if (left) { // 正向
lastElement = lastElement + step;
} else { // 反向
lastElement = (n % 2 === 0) ? lastElement : lastElement + step;
}
left=!left
n = n >> 1;
step = step << 1;
}
return lastElement;
}
第二种题解
关于第二种题解我是参考了leetcode里发题解的大佬们提到的方法,把这道题看成一道约瑟夫环的应用题,为此还去回顾了一下约瑟夫环。
分析
我们可以定义两个函数,分别为 f(n) 和 f'(n) 分别是从左往右一直删剩下的结果和从右往左一直删剩下的结果。这样我们根据两个方法的对称性,不难看出来,最后的结构会根据中心对称,也就是
f(n) + f'(n) = n + 1
再者从左往右删一次后,剩下来的数就都会变成偶数,这时候我们可以将它的每一项做一个除以2的处理,事后在乘2,但是接下去应该要从右往左删,所有能得到等式
f(n) = 2 * f'(n/2)
最后根据两个等式,两个未知量就可以解出方程,得到一个关于f(n)的递归方程式
f(n) = (n/2 + 1 - f(n/2)) * 2
然后就可以根据这个方程写出一个递归函数
/**
* @param {number} n
* @return {number}
*/
var lastRemaining = function(n) {
return n > 1 ? 2 * (Math.floor(n/2) + 1 - lastRemaining(Math.floor(n/2))) : 1
};