题目:有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; }