leetcode刷题记录-由390.消除游戏到约瑟夫环

1,168 阅读1分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

前几天在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

第一种题解

通过找规律的方法去找到每次执行后数组变化的规律

分析

首先我们看到题要去分析,题目中最重要的几个关键点是什么。

  1. lastElement为数组最后剩下的元素

    每一种情况的最后都是要剩下一个元素,所以代码中一定要有一个标记标记着最后剩下来的那个元素, 这里我们定义为 lastElement 作为最后剩下的元素,它的初始值为1

  2. 定义公差为step

    每删除一轮之后,数组的公差就会加倍,拿上方的示例一来做示范,第一行数组公差为1,第二行为2, 第三行为4...以此类推,这样我们就能够得到公差的值的公式:
    step[i+1] = step[i]*2

  3. 定义元素个数为n

    最开始传入n个元素,每轮过后删除一半的元素,因为要考虑元素个数的奇偶问题,在奇数个的时候会多删除一个,所以n每轮除以2然后向下取整,能够得到剩余元素个数的公式:
    n[i+1] = n[i]\2\lfloor n[i]\2 \rfloor

  4. 判断删除方向的参数 left

    最后我们还要知道每一次循环的时候,是从左往右还是从右往左,这里可以定义一个参数来判断,也可以不定义参数,直接通过定义一个循环轮次也就是i来判断现在是哪个方向。

示例

首先我们要知道从左和从右开始两种情况有什么不同:

  1. 从左往右的情况因为第一个元素一定会被删除,所以 lastElement 每次都加上公差。
  2. 从右往左的情况第一个元素是否被删除就要看数组的个数,奇数则被删除,偶数则不会,要做一个判断,如果被删除了依然是加上公差。

接下去我们拿一个 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 
};