引言

这道题网上的讲解都是直接抄书,没意思,所以想自己写一写,补充一下,便于自己理解。另外,大家都忽略了经典解法,虽然这种解法效率不及第二种,但是我觉得在项目当中阅读性其实很重要,牺牲一点点效率保证代码的可维护性和阅读性是值得的。与此同时,第二种方法其实需要比较好的数学功底,我不认为一般的程序员在毕业多年之后还能保证自己的数学功底,而且在面试的时候你能在短时间和高压下准确的推导吗?我相信有人能做到,但是我不知道我能不能做到,所以经典解法可以作为一种稳妥的替代方案。

题目:0,1,...,n-1 这n个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。

测试样例:

输入: 0,1 , 2, 3, 4

输出: 3

经典解法,用环形链表模拟圆圈

源码:

     public static int lastRemaining_1(int n,int m){
if(n<1||m<1) return -1;
List<Integer> list = new ArrayList<Integer>();
for(int i=0;i<n;i++){
list.add(i);
} int current = 0;//从1到m计数
int currentSize = n;//用以记录链表中元素的个数 Iterator<Integer> iterator = list.iterator();
while(currentSize>1){
for(current=1;current<=m;current++){
if(iterator.hasNext()) iterator.next();
else {
//------到链表末尾--------
iterator = list.iterator();
iterator.next();
}
}//end for
iterator.remove();
currentSize--;
}//end while
iterator = list.iterator();
return iterator.next();
}

这个思路大部分人都能立马想到,在从1到m个元素遍历时要注意有没有到达链表的末尾,若到达链表末尾,就要回到表头重新开始遍历。另外,由于java中的迭代器没有求size()的方法,所以需要自己定义一个变量currentSize记录链表中元素的个数。这种方法每删除一个数字需要m步操作,总共n个数字,因此时间复杂度是O(mn),另外还需要维持一个拥有n个元素的链表,即空间复杂度是O(n).

创新的解法

源码:

public static int lastRemaining_2(int n,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;
}

这段代码简直精简的爆,这就是数学的魅力啊!但是阅读性?要是在项目中出现这玩意,并且没有专门的注释或者讲解,你认为后期接手项目的人看到这段是什么感觉?估计砸键盘,原地爆炸了。。。

讲解:

定义函数f(n,m),表示每次在n个数字0,1,...,n-1中每次删除第m个数字最后剩下的数字。

在n个数字中,第一个被删除的数字是(m%n)-1,(这里插一句,原书上说是(m-1)%n,我觉得不对,但是这两种答案最后带到递推公式里都能得到一样的结果),我们把这个数字记为K. 在删除掉第一个元素K后,剩下的n-1个数字就是0,1,2,...,k-1,k+1,...,n-1,并且下一次删除从K+1开始计数。那么,在下一次计数的时候其实就相当于在这样一个序列中遍历:K+1,...,n-1,0, 1,... ,  K-1 。这个序列和前一个序列其实是一样的,不一样的是我们把它的顺序修改了一下而已,但是删除元素时遍历顺序是一样的。故经过若干次删除后剩下的数字和前一个序列也应该是一样的。我们把后一个序列每次删除第m个数字最后剩下的数字记为f'(n-1,m),至于为什么记为f'(n-1,m)你看到后面就懂了。那么现在我们最起码可以确定的是f(n,m)=f'(n-1,m)。

我们再来看分析这个序列:k+1,...,n-1,0,1,...,k-1 。我们将这个序列做一个映射,映射结果是形成一个从0到n-2的序列:

k+1     ->    0

k+2     ->    1

......

n-1     ->     n-k-2

0        ->     n-k-1

1        ->     n-k

......

k-1      ->     n-2

  f'(n-1,m)     f(n-1,m)

我们定义映射为p,那么p(x) = (x-k-1)%n 。 它表示如果映射前的数字是x,那么映射后的数字是(x-k-1)%n。该映射的逆映射是p-1(x)=(x+k+1)%n。既然要掌握这个方法,就要彻底搞懂,下面跟着我一起证明一遍:

证明:

令y = p(x),即 y = (x-k-1)%n

则有  y = (x-k-1) +t1n,t1属于整数,且0<= y <n

< --->   x =  y - t1n + k + 1

<---->   x =  (y+k+1) + t2n ,即 y = (x+k+1) + tn,故p-1(x) =  (x+k+1) %n

证明完毕。

现在,我们发现经过映射之后的n-1个数字是不是和原先的n个数字形式上是一样的?只不过少看一个数字n-1而已。那么,对0,1,...,n-2这n-1个数字,排成一个圆圈,从数字0开始每次删除第m个数,剩下的数字是不是可以表示成f(n-1,m)?! 现在有没有发现我们之前为什么要定义那么序列为 f'(n-1,m)? 这是要建立两次删除之间的联系!就是说原始的n个元素,在删除第一个元素k之后,按理说初始序列已经被打乱了,没有规则了;但是我们通过一个映射关系,让序列重新排列成初始序列的形式。这样只要我们找到这样的映射关系,求出两次操作之间的函数关系(迭代规律)就将问题转化成了递归问题。而递归问题的出口很好确定,当n=1时,序列只有一个元素:0,f(1,m)就是这个0!

既然有了映射关系,下面我们求两次迭代操作之间的关系,即如何由f(n-1,m)求得f(n,m)。

求解:

因为f(n,m) = f'(n-1,m),且f'(n-1,m) = (f(n-1,m)+k+1)%n,故f(n,m) = (f(n-1,m)+k+1)%n。 又因为 k = (m%n)-1,带入f(n,m) = (f(n-1,m)+k+1)%n,得:f(n,m) = (f(n-1,m)+m)%n。

因此,当n=1时,f(n,m) = 0

当n>1时,f(n,m) = [f(n-1,m)+m]%n

有了这个递推关系,是不是可以写代码了?可以由上而下的用递归,也可以由下而上的用迭代。递归在这里显然不存在子问题重复求解的问题,但是会有大量的堆栈操作,不如直接用迭代的方式。至于迭代方式的源码,上面已经给出了。 int last = 0; 是当n=1时,f(1,m)的值;后面的for循环就是自下而上的求解f(n,m)的值了。

这种思路非常复杂,但是代码尤其简洁,主要的时间都花在了分析和推导公式上了。该方法时间复杂度为O(n),空间复杂度是O(1),无论是时间复杂度和空间复杂度都要好于第一种方法。

《剑指offer》面试题45 圆圈中最后剩下的数字(Java版本)的更多相关文章

  1. 【剑指Offer】孩子们的游戏(圆圈中最后剩下的数) 解题报告(Python)

    [剑指Offer]孩子们的游戏(圆圈中最后剩下的数) 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-in ...

  2. 【剑指Offer】46、圆圈中最后剩下的数

      题目描述:   每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此.HF作为牛客的资深元老,自然也准备了一些小游戏.其中,有个游戏是这样的:首先,让小朋友们围成一个大圈.然后 ...

  3. 剑指Offer 46. 孩子们的游戏(圆圈中最后剩下的数) (其他)

    题目描述 每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此.HF作为牛客的资深元老,自然也准备了一些小游戏.其中,有个游戏是这样的:首先,让小朋友们围成一个大圈.然后,他随机指 ...

  4. [剑指offer] 46. 孩子们的游戏(圆圈中最后剩下的数)

    题目描述 随机指定一个数m,让编号为0的小朋友开始报数.每次喊到m-1的那个小朋友要出列,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友 ...

  5. 【剑指offer】孩子们的游戏(圆圈中最后剩下的数)

    题目描述 每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此.HF作为牛客的资深元老,自然也准备了一些小游戏.其中,有个游戏是这样的:首先,让小朋友们围成一个大圈.然后,他随机指 ...

  6. C++版 - 剑指Offer 面试题45:圆圈中最后剩下的数字(约瑟夫环问题,ZOJ 1088:System Overload类似)题解

    剑指Offer 面试题45:圆圈中最后剩下的数字(约瑟夫环问题) 原书题目:0, 1, - , n-1 这n个数字排成一个圈圈,从数字0开始每次从圆圏里删除第m个数字.求出这个圈圈里剩下的最后一个数字 ...

  7. Java实现 LeetCode 面试题62. 圆圈中最后剩下的数字(约瑟夫环)

    面试题62. 圆圈中最后剩下的数字 0,1,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字.求出这个圆圈里剩下的最后一个数字. 例如,0.1.2.3.4这5个数字组成一个圆 ...

  8. 【LeetCode】面试题62. 圆圈中最后剩下的数字

    题目:面试题62. 圆圈中最后剩下的数字 这题很有意思,也很巧妙,故记录下来. 官方题解思路,是约瑟夫环的数学解法: 我们将上述问题建模为函数 f(n, m),该函数的返回值为最终留下的元素的序号. ...

  9. 剑指Offer:面试题15——链表中倒数第k个结点(java实现)

    问题描述 输入一个链表,输出该链表中倒数第k个结点.(尾结点是倒数第一个) 结点定义如下: public class ListNode { int val; ListNode next = null; ...

随机推荐

  1. 作业6 团队项目之需求 (NABCD模型)

     N A B C D模型分析 WorkGroup:NewApps 组员:欧其锋(201306114305  http://www.cnblogs.com/ouqifeng/) 吕日荣(20130611 ...

  2. 分类Category的概念和使用流程

    一.了解 1.分类的概念: category:类别.类目.分类 2.分类的作用: 将1个类中不同方法分到多个不同的文件中存储 可以在不修改原来类的基础上,为这个类扩充一些方法 注意: 分类中只能增加方 ...

  3. angularJS1笔记-(18)-$http及用angular实现JSONP跨域访问过程

    官网上的解释为: The $http service is a core AngularJS service that facilitates communication with the remot ...

  4. Java中的设计模式之单例模式

    Java中的单例模式 设计模式是软件开发过程中经验的积累 一.单例模式 1.单例模式是一种常用的软件设计模式,通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控 ...

  5. ResourceBundle类读取properties文件

    1.Properties与ResourceBundle类都可以读取属性文件key/value的键值对 2.ResourceBundle类主要用来解决国际化和本地化问题,国际化时properties文件 ...

  6. exFAT移动硬盘写保护怎么去掉

    cmd命令提示符下运行chkdsk命令: 比如在E盘,则输入的命令如下: E:(冒号不可少,输入后回车) CHKDSK /F /X  (回车) 等命令执行完了,即可去掉exFAT移动硬盘写的保护.

  7. 移动web适配利器-rem

    移动web适配利器-rem 前言 提到rem,大家首先会想到的是em,px,pt这类的词语,大多数人眼中这些单位是用于设置字体的大小的,没错这的确是用来设置字体大小的,但是对于rem来说它可以用来做移 ...

  8. Linux内核0.11 bootsect文件说明

    一.总体功能介绍 这是关于Linux-kernel-0.11中boot文件夹下bootsect.s源文件的说明,其中涉及到了一些基础知识可以参考这两篇文章. 操作系统启动过程 软盘相关知识和通过BIO ...

  9. VMware 虚拟机 不能上网 CentOS 6.5 Windows 7上面安装了VMware,然后安装了CentOS系统,安装完了无法上网;

    今天想要学习一下大数据的知识,在windows 7上面 安装了VMware,然后安装了Centos系统,但是发现安装完了,无法上网 我在Centos上面 使用 ping www.baidu.com 始 ...

  10. java的break跳出多层循环

    记得大一的时候,语言学的不好,碰到了需要跳出双层循环的时候,就没有了办法.因为老师讲了goto然后说不要用goto...  自己就一直感觉这种跳出多层循环的想法是不可取的(好蠢) 下面用java代码的 ...