题目
0,1,… ,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
第一种:递归方法
剑指offer书上的思路:首先定义了一个函数f(n,m),这个函数代表的是圆圈数字为n,每次从圆圈里删除第m个数时,最后剩下的一个数字的标号。
考虑递归,要想用递归,就要寻找两个条件1.base case 2.f(n,m)和f(n-1,m)的关系,即如何让问题规模减少。
第一个条件base case很容易,当圆圈中只有一个数字的时候,这个数字就是剩下的数字,即标号为0.
第二个条件就要开始分析删除过程,一开始圈中有n个数字,标号为0~n-1,此时要删除第m个数,容易得第一个被删除后的元素应该为(m-1)%n(此处第m个数的下标应为m-1)。令k = (m-1)%n,那么删除第一个数后,圆圈中剩余的数下标为0,1,2,…,k-1,k+1,…,n-1.此时要进行第二次删除,这个时候圆圈中的数字个数为n-1,要删除第m个数,这不就是我们要找的规模减少吗?那么第二次删除的数字下标是f(n-1,m)吗?显然不是,为什么呢?因为我们上边定义的函数f(n,m)的使用条件是从下标0开始到n-1的数中删除第m个数,而第二次删除的时候,我们是从下标k+1开始,这里就是这个算法最难的部分,映射转换。
既然f函数规定的是从0开始,那我们将k+1映射为0,相应的,k+2映射为1…依次类推,原来的最后一个下标n-1映射为n-k-2, 0映射为n-k-1, 1映射为n-k, k-1映射为当前圆圈的最后一个下标n-2。由此可得到上一轮与本轮的映射关系,定义为p,记上一轮的下标为x’,本轮下标为x,则有,x = p(x‘) = (x’-k-1)%n; 可以得到p的逆,记为p*,即根据本轮的下标推出上一轮的下标,x‘=p*(x)=(x+k+1)%n。此时就得到了相邻两轮删除时下标的映射关系。
再以第二轮为例,可以套用函数f,得到最后删除的数字为f(n-1,m),此时这个数字的下标是在第二轮第一个数从0开始的前提下计算出来的。根据上一段的映射关系,可以得到第一轮这个数字的下标应该是p*(f(n-1,m)),即(f(n-1,m)+k+1)%m,第一轮最后剩下的数字和第二轮应该是同一个,所以再对第一轮套用f函数,可得到剩下的数字下标为f(n,m). 即f(n,m) = (f(n-1,m)+k+1)%n, k = (m-1)%n代入,f(n,m) = (f(n-1,m)+m)%n,这样,递推关系就找到了。
public class Solution {
public int LastRemaining_Solution(int n, int m){
if ( n < 1 || m < 1 ) {
return -1;
}
if ( n == 1 ) {
return 0;
} else {
return (LastRemaining_Solution( n - 1, m ) + m ) % n;
}
}
}
第二种:用数组模拟链表环
剑指offer上说,用链表环结构,比较复杂。所以我们可以用数组来模拟,通过下标的转换来实现环结构。
public class Solution {
public int LastRemaining_Solution(int n, int m){
if ( n < 1 || m < 1 ) {
return -1;
}
int[] arr = new int[n];
int i = -1, step = 0, count = n;
while ( count > 0 ) {
i++;
if ( i == n ) i = 0;
if ( arr[i] == -1 ) continue;
step++;
if ( step == m ) {
arr[i] = -1;
step = 0;
count--;
}
}
return i;
}
}
n为圈中的数字个数,m为要删除的数,arr数组用来模拟圆圈,i记录数组下标,当i达到n的时候,将其变为0,实现循环。step为进行的步数,当step等于m的时候,删去元素,这里删去元素的方式并不是从数组中删除,而是将对应的值置为-1来表示该元素已被删除,从而跳过链表中的删除元素的步骤,避开了数组增删慢的缺点。
数组下标i为什么要从-1开始呢?因为如果从0开始,0位置的数字要进行操作,那么i++语句就要放到最后,但是这样第11行判断跳过的语句就会在条件成立时,在while中形成死循环,所以i++一定要在continue之前,但是我们又不能跳过第一个数字,只能把i的初始值设为-1.