剑指offer:孩子们的游戏(圆圈中最后剩下的数)

 

题目描述

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。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;
    }
}

 

参考:

https://blog.csdn.net/u010429424/article/details/73695062

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值