Java实现Josephus约瑟夫环问题的算法

Java实现Josephus约瑟夫环问题的算法

前言

  • 语言:Java
  • 环境:IntelliJ IDEA
  • JDK版本:1.8
  • 源码:GitHub

问题概述

N个人围成一圈,规定报数为M,第一个人从1开始报数,报数到M的人将被杀掉,再从下个人继续从1报数,如此循环,最后剩下一个,其余人都将被杀掉。求被杀人的顺序和最后幸存者的编号

基于链表的解法

用链表求解,我们首先要想到的是,这个问题是循环报数,因此我们应该选择循环链表,由于报数只需要朝着同一方向报数,所以不用考虑双向循环链表。

确定了用循环链表,那么这个链表上的每个节点就保存的是参与者信息,例如编号。我们规定参与者个数为size,报数为k的人死亡,那么,我么需要考虑下列问题:

  • 如何完成报数:使用一个num变量计数,当为k时重置num
  • 如何删除死亡人员:单向链表删除一个节点需要一个辅助指针,并指向要删除节点的前一个节点,我们定义为pre指针
  • 如何确定人员死亡后,从下个成员重新报数:我们定义为target指针,指向报数人员

流程为:先创建循环链表,该链表的节点保存参与人员编号,节点个数为size,创建过程中让pre指针始终指向刚创建的节点,这样创建完成后pre就指向最后一个节点,让target指向链表创建的第一个结点,保持pre始终指向target的前一个节点。流程开始,num计数,当numk时,通过pre删除死亡人员节点,target指向死亡人员节点的下一个节点。重复此过程,直到节点删除剩余一个,最后这个节点,pretarget都指向他,即该节点的next也指向自己,游戏结束。

    /**
     * 基于链表的实现
     * @param size 参与人数
     * @param k 报号
     */
    public static void josephousLinked(int size,int k){
        Node first = null;  //指向编号为0的人的指针
        Node pre = null;    //指向即将死亡人员前一名人员的指针
        Node target = null; //指向即将死亡人员的指针
        int num = 0;    //计数器,0~k
        if(size>0){ //初始化第一个人员
            first = new Node(0);
            pre = first;
        }
        for(int i = 1;i<size;i++){  //初始化其余人员,最终pre指向编号最后的人
            Node node = new Node(i);
            pre.next = node;
            pre = node;
        }
        target = first; //初始化target指针,从0号开始报数
        pre.next = target;  //将链表连接为循环链表
        while(target!=pre){ //当target和pre指向同一个人时,游戏结束,这个指向的人存活
            num++;  //报数
            if(num==k){ //报到k的人死亡
                System.out.println("第"+target.id+"号人死亡");   //打印信息
                pre.next = target.next; //将这个人从链表移除
                target = pre.next;
                num = 0;    //计数器归零
                continue;   //结束本次报数,这时target已经指向已死亡人员的下个人了
            }
            target = target.next;   //如果没有报数到k,则移动target到下个人
            pre = pre.next; //如果没有报数到k,则移动pre到下个人(target的前一个人)
        }
        System.out.println("幸存者为"+target.id+"号");
    }
    static class Node{
        int id;
        Node next;
        public Node(int id){
            this.id = id;
        }
    }

基于数组的解法

使用数组求解,先构建一个大小为参与人数size大小的数组,我们用数组索引表示这些人的编号。这个数组中存放的是每个人的状态,由于数组刚构建时为0,因此我们规定,数组值为0,则这个人存活,若为1,则这个人死亡,报数为k的人死亡,直到数组中只有一个存活的人,且这个人索引对应的数组值为0,则游戏结束。我们需要考虑以下问题:

  • 如何完成报数:使用一个num变量计数,当为k时重置num
  • 如何删除死亡人员:将这个人员索引对应的数组值置为1,且遍历到数组值为1的人,只增加索引,不增加计数器num
  • 如何确定人员死亡后,从下个成员重新报数:死亡人员索引+1,需要考虑到数组越界,因此需要取模
    /**
     * 基于数组的实现
     * @param size 参与人数
     * @param k 报号
     */
    public static void josephousArray(int size,int k){
        int num = 0;    //计数器,0~k
        int index = 0;  //游标
        int sur = size; //存活人数
        int[] array = new int[size];    //所有人组成的数组
        while(true){
            if(array[index%size] == 1){ //如果这个人已经死亡,则下一位开始
                index++;
                continue;
            }
            num++;//当前人员报数
            if(num==k){ //如果报数到k时,这个人员死亡
                array[index%size] = 1;  //标记死亡
                System.out.println("第"+index%size+"号人死亡");  //打印信息
                num = 0;    //重置计数器
                sur--;  //存活人员减少
            }
            if(sur==1&&array[index%size]==0){   //到最后一人时停止报数,游戏结束
                break;
            }
            index++;
        }
        System.out.println("幸存者为"+index%size+"号");
    }

基于递归的解法

在使用递归之前,我们先要找到约瑟夫环中的规律,如果有下面的数组:

0,1,2,3,4

报数为3的人死亡,则第一个死亡的人一定是2号,也就是说,当给定一个人员总数size,和报数k,那么死一个死亡人员一定是(k-1)%size,这里考虑到报的数k-1可能会大于size,所以要取模。当2号死亡后,我们重新排列数组:

3,4,0,1

我们对这个新数组重新排号为

0,1,2,3

我们可以理解为,要求出0,1,2,3这个数组的最后死亡的人(也就是幸存者),通过一定的对应关系,就能推导出3,4,0,1的幸存者,那么要求出0,1,2,3这个数组的最后死亡的人,我们又可以类比为第一次的做法,重新排序,只需要求出0,1,2的最后死亡的人,直到类比到求出数组只有一个人员0的最后一个死亡的人,显而易见,数组只有0,则不管报什么号,最后一个死亡的都是他,也可以说幸存者就是他。我们先求出简单解:

F(1) = 0 (size=5,k=3)

F(2) = 1 (size=5,k=3)

F(2) =( F(1)+k)%size

我们便得到了递推公式:F(size) = (F(size-1)+k)%size,例如:要求出第n个人的第n个死亡人员编号,只需要知道第n-1个人第n-1个死亡人员的编号

    /**
     * 基于递归的实现
     * @param size 参与人数
     * @param k 报号
     */
    public static void josephousRecursion(int size,int k){
        for(int i = 1;i<size;i++){  //调用递归方法,实际上如果只需要求出最后存活的人,就不需要这个循环
            System.out.println("第"+recursion(size,k,i)+"号人死亡");
        }
        System.out.println("幸存者为"+recursion(size,k,size)+"号");
    }

    /**
     * 递归方法
     * @param size 当前圈中的人数
     * @param k 报号
     * @param i 第i个死亡的人
     * @return  返回第size个人第i次死亡的人的位置
     */
    private static int recursion(int size,int k,int i){
        if(i==1){
            return (k-1)%size;
        }else{
            return (recursion(size-1,k,i-1)+k)%size;
        }
    }
}

转载于:https://my.oschina.net/rawlins/blog/3102836

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值