剑指Offer+第45题+圆圈中最后剩下的数字+java

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

测试样例:

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

输出: 3

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

既然题目中有一个数字圆圈,很自然的想法就是用个数据结构来模拟这个圆圈。在常用的数据结构中,我们很容易的想到环形链表。我们可以创建一个共有n个结点的环形链表,然后每次都从这个链表中删除第m个结点

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

方法2,更优的解法,很难想到(反正我想不到)

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

在n个数字中,第一个被删除的数字是(m%n)-1,我们把这个数字记为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),无论是时间复杂度和空间复杂度都要好于第一种方法

代码:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Offer45 {

	//方法1
	public int lastRemaining(int n, int m){
		if(n<1 ||m<1)
			return -1;
		List<Integer> list = new ArrayList<>();
		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();
				}
			}
			iterator.remove();
			currentSize--;
		}
		iterator = list.iterator();
		return iterator.next();
	}
	
	//方法2,确实很牛逼,但是我肯定自己想不出来
	public 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;
	}
	
	public static void main(String[] args) {

		Offer45 of45 = new Offer45();
		
		//功能测试,1,输入的m<n
		int n1 = 5;
		int m1 = 3;
		//System.out.println(of45.lastRemaining(n1, m1));
		System.out.println(of45.lastRemaining_2(n1, m1));
		
		//功能测试,2,输入的m<n
		int n2= 5;
		int m2 = 2;
		//System.out.println(of45.lastRemaining(n2, m2));
		System.out.println(of45.lastRemaining_2(n2, m2));
		
		//功能测试,3,输入的m=n
		int n3 = 5;
		int m3 = 5;
		//System.out.println(of45.lastRemaining(n3, m3));
		System.out.println(of45.lastRemaining_2(n3, m3));
		
		//功能测试,4,输入的m>n
		int n4 = 5;
		int m4 = 6;
		//System.out.println(of45.lastRemaining(n4, m4));
		System.out.println(of45.lastRemaining_2(n4, m4));
		
		//特殊输入测试,5,圆圈中有0个数字
		int n5 = 0;
		int m5 = 6;
		//System.out.println(of45.lastRemaining(n5, m5));
		System.out.println(of45.lastRemaining_2(n5, m5));
		
		//性能测试,6,n=4000,m=997
		int n6 = 4000;
		int m6 = 997;
		//System.out.println(of45.lastRemaining(n6, m6));
		System.out.println(of45.lastRemaining_2(n6, m6));

	}

}

运行结果:

3
2
1
3
-1
1027

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值