题目描述
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
本题为约瑟夫环问题,通常有两种方法来解决这个问题。
第一类方法:用环形链表模拟圆圈。(环形链表的构造可以延伸出多种不同的解法)
第二类方法:分析每次被删除的数字的规律并直接计算出圆圈中最后剩下的数字。
思路1:构造环形链表模拟圆圈(时间复杂度o(mn),空间复杂度o(n))
实现1:直接用java数据结构中的LinkedList来模拟实现
其中,在while循环中,每一次应该删除的index的计算:index=(index+(m-1))%list.size(),这是因为:
第一次删掉的位置是从0开始数m-1个位置,以后每次从删掉的下一个节点开始取,所以每次要在indext的索引处加上m-1,因为是环,所以加了以后对链表长度取余。这样就可以直接计算出每次应该删除的索引位置
代码1:
import java.util.LinkedList;
public class Solution {
public int LastRemaining_Solution(int n, int m) {
if(n<1||m<1){
return -1;
}
//直接用数据结构中的LinkedList
LinkedList<Integer> list=new LinkedList<>();
for(int i=0;i<n;i++){
list.add(i);
}
int index=0;
while(list.size()>1){
index=(index+m-1)%list.size();
list.remove(index);
}
return list.size()==1?list.get(0):-1;
}
}
代码2:链表模拟环的实现
import java.util.LinkedList;
public class Solution {
public int LastRemaining_Solution(int n, int m) {
if(n==0||m<1){
return -1;
}
LinkedList<Integer> list=new LinkedList<>();
for(int i = 0; i < n; i++) {
list.add(i);
}
int count = 0;
int index = 0;
while(list.size() > 1) {
count++;
if(count % m == 0) {
list.remove(index--);
}
if(++index == list.size()){
index = 0;
}
}
return list.get(0);
}
}
实现2:用数组模拟环来实现。
当数到第m个数时,就将对应索引位置的元素置位-1,且剩余元素-1(相当于从整个环中删除了一个元素)。当索引自加超过元素总数时,则将索引置位0(从头开始,从而模拟了环的实现),而从头开始后,通过判断元素值是否为-1来判断这个索引是否有效(没有被删除过)。
public class Solution {
public int LastRemaining_Solution(int n, int m) {
//用数组模拟环
if(n<1||m<1){
return -1;
}
int[] array=new int[n];
int index=-1,count=0,leftCount=n;
while(leftCount>0){
index++;
if(index>n){
index=0;//数组模拟环的实现
}
if(array[index]==-1){
continue;
}
count++;
if(count==m){
array[index]=-1;
count=0;
leftCount--;
}
}
return index;//返回跳出循环时的index
}
}
实现3:手动构造循环链表实现
如果面试官要求不能使用已有的数据结构求解,那么我们就需要自己手动构造一个循环链表。而在循环链表种树删除第m个元素很简单,每次数到第m个元素的前一个元素时,直接将这个前驱结点的next指向下一个节点的next即可删除。
class ListNode{
int val;
ListNode next=null;
public ListNode(int val){
this.val=val;
}
}
public class Solution {
public int LastRemaining_Solution(int n, int m) {
//手动实现循环链表解决
if(n<1||m<1){
return -1;
}
ListNode head=new ListNode(0);//头结点,值为0
ListNode pre=head;
ListNode temp=null;
for(int i=1;i<n;i++){
temp=new ListNode(i);
pre.next=temp;//连接
pre=temp;//头指针向后移动一位
}
temp.next=head;//首尾相连,构成循环链表
ListNode temp2=null;
while(n!=1){
temp2=head;
for(int i=1;i<m-1;i++){
temp2=temp2.next;
}
//找到第m个结点的前驱结点,则进行删除操作
temp2.next=temp2.next.next;//删除第m个节点
head=temp2.next;//更新头结点
n--;
}
return head.val;
}
}
思路2:数学分析的方法
参考:https://blog.csdn.net/qq_41822235/article/details/82382422
取余运算的一些性质需要熟悉:
一、某数取余多次与取一次相等,例如5%2=1,而5%2%2%2=1;
二、取余运算满足结合律,例如a%n - 1=(a-1)%n (n > 1);
三、如果对于一个明显不在[0, n-1]区间的数x,在该区间的同余数有且只有1个。例如x = -2,-2%n=n-2
定义一个关于n和m的方程f(n,m),表示每次在n个数字0,1,...n-1中每次删除第m个数字后剩下的数字,
在n个数字中,第一个被删除的数字是(m-1)%n,将这个数字记为k,且下次删除的数字从k+1开始计数,即k+1,...,n-1,0,1,...k-1,该序列最后剩下的数字也应该是关于n,m的函数,因为这个序列的规律和最初序列的规律不一样,所以将这个函数记为f`(n-1,m).
而最初序列剩下的数字一定f(n,m)是删除一个数字之后的序列最后剩下的数字,即f(n,m)=f`(n-1,m).
剩下n-1个数字,分别是0,1,...,k-1,k+1,...,n-1。调整顺序,作出如下处理。形成一个0-n-2的序列,
将映射定义为p,则p(x)=(x-k-1)%n,它表示映射前的数是x,映射后的数字是(x-k-1)%n,该映射的逆映射是p逆(x)=(x+k+1)%n.
推导公式见图中,铅笔部分。
因此最后我们得到上图中一个递推公式,用循环的方式实现这个公式如下:
实现1:时间复杂度o(n),空间复杂度o(1)
public class Solution {
public int LastRemaining_Solution(int n, int m) {
//约瑟夫环问题,利用数学分析总结
if(n<1||m<1){
return -1;
}
int ret=0;
for(int i=2;i<=n;i++){
ret=(ret+m)%i;
}
return ret;
}
}
参考: