题目:有0,1,...,n-1这n个数字排成一个圈,从数字0开始每次从这个圈中删除第m个数字,求出这个圆圈里剩下的最后一个数字。
方法一:用环形链表模拟圆圈
int lastRemaining(unsigned int n,unsigned int m){
if(n < 1 || m < 1)
return -1;
unsigned int i = 0;
list<int> ilist;
for(;i < n;i++)
ilist.push_back(i);
list<int>::iterator iter = ilist.begin();
while(ilist.size > 1){
for(i = 1; i < m;i++){
iter++;
if(iter == ilist.end())
iter = ilist.begin();
}
list<int>::iterator iterNext = ++iter;
if(iterNext = ilist.end())
iterNext = ilist.begin();
iter--;
ilist.erase(iter);
iter = iterNext;
}
return (*iter);
}
时间空间复杂度分析:每删除一个数字需要m步运算,总共有n个数字,故总的时间复杂度为O(mn),同时还需要一个辅助链表来模拟圆圈,其空间复杂度为O(n)。
方法二:通过数学方法找出要删除数字的规律
我们定义一个函数f(n,m,i),它的意义是对于一个有n个元素的环,每次删除第m个元素,删除i次之后最后剩下的元素为f(n,m,i)。由此我们知道f(n,m,i) = f(n-1,m,i-1);
①设最初的环由0 1 2 ... n-1组成,删除的第m个元素为下标为(m-1)%n的元素,我们设为k,即k=(m-1)%n;
②去除第m个元素后剩下的元素为0 1 2 ... k-1 k+1... n-1,接下来我们以第k+1个元素为起点,再找第m个元素,也即k+1 k+2 ... n-1 0 1... k-1
如果我们对其进行一下变换
k+1 -> 0
k+2 -> 1
...
n-1 -> n-k-2
0 -> n-k-1
....
k-1 -> n-2
也就是说左边的x经过f(x) = (x-k-1)%n得到右边的数(这里其实是下标,但是因为数和下标相同,所以也可以认为是数)。 其逆映射为p(x)=(x+k+1)%n。
③由于映射后的序列和最初的序列具有同样的形式,即都是从0开始的连续序列,因此仍然可以用函数f表示,记为f(n-1,m),根据我们的映射规则,映射之前的序列中最后剩下
的数字f ' (n-1,m) = p[f(n-1,m)] = [f(n-1,m)+k+1]%n,把k=(m-1)%n带入得到f(n,m) = [f(n-1,m)+m]%n。
④由此我们找到了一个递归公式,要得到n个数字的序列中最后剩下的数字,只需要得到n-1个数字的序列中最后剩下的数字,并依此类推。当n=1时,也就是序列中开始只有一
个数字0,那么很显然最后剩下的就是0。
循环实现代码:
int lastRemaining(unsigned int n,unsigned int m){
if(n < 1 || m < 1)
return -1;
int last = 0;
for(int i = 2; i < n; i++){
last = (last + m) % i;
}
return last;
}
方法同上类似,只不过是用递归实现:
假设n=10,m=3,那么0 1 2 3 4 5 6 7 8 9在第一个数字出列后为0 1 3 4 5 6 7 8 9,即3 4 5 6 7 8 9 0 1(*),我们要将其转化为0 1 2 3 4 5 6 7 8 (**)
我们发现(**)通过((**)+3)/10可以转化为(*),也就是我们求出9个人中第9次出环的编号,最后经过上面的转化就可以得到10个人经过10次出环的编号了。
设f(n,m,i)为n个人的环,报数为m,第i个人出环,则f(10,3,10)是我们需要的结果,
当i = 1时,f(n,m,i) = (n+k-1)%n;
当i != 1时,f(n,m,i) = (f(n-1,m,i-1)+m)%n;
int fun(int m,int k,int i){
if(i==1)
return (m+k-1)%m;
else
return (fun(m-1,k,i-1)+k)%m;
}
int main(int argc, char* argv[])
{
for(int i=1;i<=10;i++)
printf("第%2d次出环:%2d\n",i,fun(10,3,i));
return 0;
}