给定一个从1 到 n 排序的整数列表。

首先,从左到右,从第一个数字开始,每隔一个数字进行删除,直到列表的末尾。

 

第二步,在剩下的数字中,从右到左,从倒数第一个数字开始,每隔一个数字进行删除,直到列表开头。

 

我们不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。

返回长度为 n 的列表中,最后剩下的数字。

 

示例:

输入:
n = 9,
1 2 3 4 5 6 7 8 9 (1,3,5,7,9被删除)
2 4 6 8 (8,4被删除)
2 6 (2被删除)
6 (剩余6)

输出:
6

答案:

 1public int lastRemaining(int n) {
2    boolean left = true;
3    int remaining = n;
4    int step = 1;
5    int head = 1;
6    while (remaining > 1) {
7        if (left || ((remaining & 1) == 1)) {
8            head = head + step;
9        }
10        remaining = remaining >> 1;
11        step = step << 1;
12        left = !left;
13    }
14    return head;
15}

解析:

题描述的很清晰,就是先从左往右每隔一个就删除一个数字,然后再从右往左每隔一个删除一个数字……一直这样循环下去,直到最后剩下一个数字为止。

 

在计算机编程中有个非常著名的算法题就是“约瑟夫环问题”,也称“丢手绢问题”,如果对约瑟夫环问题比较熟悉的话,那么今天的这道题也就很容易理解了。如果不熟悉的话也没关系,我们今天就详细分析一下这道题。关于约瑟夫环问题不在今天所讲的范围之内,后续有时间我们在单独讲解。

 

这题如果使用双向链表或者是双端队列很好解决,因为双向链表既可以从前往后删除也可以从后往前删除,当然这两种方式都需要先初始化,今天我们讲的这种方式是既没有使用链表也没有使用数组。

 

我们来看下上面的代码,直接看可能不太直观,我们可以把n想象成一个长度为n的数组,数组的元素是1,2,3,4,5……n,我们只需要记录下每次删除一遍之后数组的第一个元素即可,当remaining==1的时候就会退出while循环,最后返回数组的仅有的一个元素即可(这只是我们的想象,实际上操作的并不是数组,也没有删除,只是记录,但原理都类似)。

 

    boolean left = true;

代码left判断是否是从左往右删除,如果为true表示的是从左往右删除,如果为false表示的是从右往左删除。

 

    int remaining = n;
    int step = 1;
    int head = 1;

代码remaining表示剩余的个数。step表示每次删除的间隔的值,不是间隔的数量,比如1,2,3,4,5,6,7,8。第一次从左往右删除的时候间隔值是1,删除之后结果为2,4,6,8。第二次从右往左删除间隔值就变为2了,删除之后结果是2,6。然后第3次就变成4了。head表示的是记录的剩余数字从左边数第一个的值。

 

1    while (remaining > 1) {
2        if (left || ((remaining & 1) == 1)) {
3            head = head + step;
4        }
5        remaining = remaining >> 1;
6        step = step << 1;
7        left = !left;
8    }

第5-7行代码很好理解,remaining表示的是剩余个数,每次删除的时候都会剩余一半,所以除以2,也可以表示为往右移一位。step上面说了表示的是间隔值,每次循环之后都会扩大一倍,left就不在说了,一次往左一次往右……一种这样循环。

 

我们主要来看下第2-4行代码,如果是从这边循环,那么第一个肯定是会被删除的,第二个会成为head,而第二个值就是head+step;如果从右边开始循环,如果数组的长度是奇数,那么第一个元素head也是要被删除的,所以head值也要更新,代码remaining&1==1判断remaining是否是奇数。

 

我们以n=14为例,画个图来看下会更直观一些

365,消除游戏_编程开发

上面代码变量比较多,实际上我们还可以改的更简洁一些

1public int lastRemaining(int n) {
2    int first = 1;
3    for (int step = 0; n != 1; n = n >> 1, step++) {
4        if (step % 2 == 0 || n % 2 == 1)
5            first += 1 << step;
6    }
7    return first;
8}

注意这里的step不是删除的间隔值,他是表示的是每删除一遍就会加1,比如最开始从左往右删除的时候step是0,然后再从右往左删除的时候是1,然后再从左往右删除的时候是2,然后再从右往左删除的时候是3……,一直这样累加。代码很好理解,就不在过多解释。

 

下面我们再来换种思路想一下

1,当我们从左往右消除的时候,比如[1,2,3,4]第一次从左往右消除的时候结果是[2,4],也就是2*[1,2]

或者[1,2,3,4,5]第一次从左往右消除的时候结果也是[2,4],也就是2*[1,2]

所以我们只需要计算数组前面一半的结果然后再乘以2即可。

2,当我们从右往左消除的时候,如果数组是偶数,比如[1,2,3,4,5,6]消除的结果是[1,3,5],也就是2*[1,2,3]-1,如果数组是奇数的话,比如[1,2,3,4,5,6,7]消除的结果是[2,4,6],也就是2*[1,2,3]。所以明白了这点,代码就很容易想到了

 1public int lastRemaining(int n) {
2    return leftToRight(n);
3}
4
5private static int leftToRight(int n) {
6    if (n <= 2)
7        return n;
8    return 2 * rightToLeft(n / 2);
9}
10
11private static int rightToLeft(int n) {
12    if (n <= 2)
13        return 1;
14    if (n % 2 == 1)
15        return 2 * leftToRight(n / 2);
16    return 2 * leftToRight(n / 2) - 1;
17}

 

我们再来思考一个问题,可以找一下规律

1,当n个数的时候,假设我们从左往右执行,剩下的数字记为f1(n)(从数组[1,2,……n]开始),从右往左执行,剩下的数字是f2(n)(从数组[n,n-1,……1]开始)。

 

2,如果我们记f1(n)在数组[1,2,……n]中的下标为k,那么f2(n)在数组中[n,n-1,……1]的下标也一定是k。所以我们可以得到f1(n)+f2(n)=n+1。

 

3,对于n个元素,执行一次从左往右之后,剩下的[2,4,……n/2]就应该从右往左了,我们记他执行,剩下的数字是f3(n/2),所以我们可以得到f1(n)=f3(n/2),f3(n/2)=2*f2(n/2);

 

4,根据上面的3个公式

(1):f1(n)+f2(n)=n+1

(2):f1(n)=f3(n/2)

(3):f3(n/2)=2*f2(n/2)

我们可以得出f1(n)=2*(n/2+1-f1(n/2));并且当n等于1的时候结果就是1,所以代码如下,非常简单

1public int lastRemaining(int n) {
2    return n == 1 ? 1 : 2 * (1 + n / 2 - lastRemaining(n / 2));
3}

对于这道题的理解我们还可以来举个例子,比如[1,2,3……n],如果从左开始结果是k,那么从右开始结果就是n+1-k。比如[1,2,3,4,5,6,7,8,9,10]第一遍从左到右运算之后是[2,4,6,8,10],假如[1,2,3,4,5]从左到右的结果是f(5),那么他从右到左的结果就是5+1-f(5),也就是6-f(5),所以[2,4,6,8,10]从右到左的结果就是2*(6-f(5)),所以我们可以得出f(10)=2*(6-f(5)),所以递推公式就是f(n)=2*(n/2+1-f(n/2))。

我们还可以把上面递归的代码改为非递归,这个稍微有一定的难度

 1public int lastRemaining(int n) {
2    Stack<Integer> stack = new Stack<>();
3    while (n > 1) {
4        n >>= 1;
5        stack.push(n);
6    }
7    int result = 1;
8    while (!stack.isEmpty()) {
9        result = (1 + stack.pop() - result) << 1;
10    }
11    return result;
12}

 

下面再来思考一下,看能不能再优化一下,我们让left(n)=left[1,2,3,……n]表示从左往右执行之后,剩下的数字,right(n)=right[1,2,3,……,n]表示从右往左执行之后,剩下的数字,所以我们可以得出一个结论

1,left(1)=right(1)=1;

2,left(2k)=left[1,2,3,……2k]=right[2,4,6,……2k]=2*right(k);

3,left(2k+1)=left[1,2,3,……2k,2k+1]=right[2,4,6,……2k]=2*right(k);

4,right(2k)=right[1,2,3,……2k]=left[1,3,5,……2k-1]=left[2,4,6,……2k]-1=2*left(k)-1

5,right(2k+1)=right[1,2,3,……2k,2k+1]=left[2,4,6,……2k]=2*left(k)。

6,left(4k)=left(4k+1)=4*left(k)-2;

7,left(4k+2)=left(4k+3)=4*left(k);

搞懂了上面的规律,代码就呼之欲出了,下面我们来看下代码

1public int lastRemaining(int n) {
2    if (n < 4)
3        return (n == 1) ? 1 : 2;
4    return (lastRemaining(n / 4) * 4) - (~n & 2);
5}

 

我们再来看最后一种解法,也是一行代码搞定

1public int lastRemaining(int n) {
2    return ((Integer.highestOneBit(n) - 1) & (n | 0x55555555)) + 1;
3}

如果对约瑟夫环问题比较熟练的话,那么这种解法就比较好理解了,其实他就是约瑟夫环中k=2的一个问题。