原题链接:https://leetcode-cn.com/problems/elimination-game/

列表 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 <= 109


解题思路


首先想到的是模拟,即按题目描述的步骤重复删除数字,直至剩下一个数字为止。实际操作时,由于数组元素只会出现1~n,所以只需要将待删除的数字置为0或一个负数即可,不必真正删除该数字。这种方式每次删除数字时需要遍历一轮数组,由于每次删除数组剩余元素的一半,所以一共需要删除 log2n 次,整体算法复杂度为O(nlogn)级别,考虑到n的数量级为10^9级别,算法极有可能超时。

实际操作时可以发现,从左到右删除数字时,最左边的数字必定会被删除,最右边的数字根据元素个数有两种情况:当元素个数是偶数时,这个数字不会被删除,当元素数个是奇数时,这个数字会被删除。同样,从右到左删除数字时,最右边的数字必定会被删除,而最左边的数字也和上面一样,只有当元素个数是奇数时才会被删除。

由此可以想到使用双指针法,使用左右两个指针指向每轮操作时最左和最右的元素,然后两个指针向中间移动,当指针相遇时返回相遇的数字。需要记录的变量有当前的删除方向,删除的步长,以及左右指针的位置,具体代码及注释如下:

class Solution {
public:
    int lastRemaining(int n) {
        int dir = 0; // 操作方向, 0:左->右,1:右->左
        int step = 1; // 步长,每操作一次乘2
        int left = 1, right = n; 

        while(left < right) {
            int count = (right - left) / step + 1; // 计算当前剩余元素的个数
            if(dir == 0) {
                // 从左向右操作时,左指针必移动一个步长,右指针只在元素个数为奇数时移动
                left += step;
                if(count % 2) {
                    right -= step;
                }
            } else {
                // 从右向左操作时,右指针必移动一个步长,左指针只在元素个数为奇数时移动
                right -= step;
                if(count % 2) {
                    left += step;
                }
            }
            dir = !dir;
            step = step * 2;
        }

        return right; // 左右指针相遇后,返回左或右都可以
    }
};


上面的算法每次删除只需要移动左右指针,不需要遍历整个数组,所以算法复杂度降为O(logn)。