【算法】约瑟夫环问题与Java实现

1,455 阅读4分钟

1. 介绍

  约瑟夫环大概介绍如下:n个人围成一圈,第一个人从1开始报数,报m的人出局,下一个人接着从1开始报数,如此循环,直到所有人出局。详细介绍看这里

  解决约瑟夫环问题,我觉得可以分两种情况。

  1. 只求结果,即只要求算出最后出局的人。

  2. 求全过程,即要算出每轮出局的人。

2. 情况一:只求结果

  先介绍约瑟夫问题的两种特例,约定符号含义:n代表总人数,m表示报该数的人出局,给这n个人从1开始编号(注:这里从1开始编号是为了方便理解)。

2.1 特例1:n是2的次幂,m是2

  • 当n是2时,最后出局的人(胜利者)显然是1。

  • 当n是4时,那么出局顺序为2、4、3,胜利者是1。

  • 当n是8时,那么出局顺序为2、4、6、8、3、7、5,胜利者是1。

  • ......

不难发现,在这种特例下,胜利者始终是1号。

2.2 特例2:n是任意数,m是2

  比如当n为12,m为2时,其约瑟夫环如下图所示:

001-01.png   当8号出局后,就剩下8人了,而2^3=8,于是此时的情况就符合特例1了。那么8号的下一位9号就相当于特例1里的1号,所以9号就是最终胜利者。
  综上所述,当m为2时,解决约瑟夫环问题的思路就是找出什么时候剩下的人数为2的次幂,其下一轮报1的人就是最终胜利者。

2.3 任意情况:n和m都是任意数

  在这种情况下,解决问题的方法就是使用递归。为了方便计算,这里从0开始编号,即0~(n-1)。比如当n=7(即编号为0~6),m=3时,其每次出局的情况如下图所示: 001-02.png   每出局一人,就把剩余的所有人都往左移m位(这里的m是3),这样每次的出局情况就用黄色格子标识。显然可以看出,无论什么情况下,最后胜利者(红字)所在的位置必定是0号位,那么他在上一轮(第6轮)的位置就是(0+3)%2=1。这里"+3"的含义就是右移,因为他是通过左移来到这轮的,要想返回到上一轮就得右移,但要考虑越界的情况所以要对n求余,n是该轮所剩人数。在第5轮的位置就是(1+3)%3=1,如此循环就能得出胜利者在刚开始的位置。
  至此,计算约瑟夫环结果的思路已全部给出,下面给出实现的Java代码。

2.4 Java代码

public class Test01 {
    /**
     * 约瑟夫环问题,只求结果
     * @param n 该轮所剩的人数
     * @param m 报该数的人出局
     * @return 胜利者的下标,从0开始
     */
    public static int josephus(int n, int m) {
        if (m == 2) {       // m=2时的特殊情况
            int k = 1;
            while (k <= n) {
                k = k << 1;
            }
            k = k >> 1;
            return (n - k) * 2;
        } else {            // 普遍情况,使用递归
            if (n == 1) {
                return 0;
            } else {
                return (josephus(n-1, m) + m) % n;
            }
        }
    }
    public static void main(String[] args) {
        // 由于函数返回的是从0开始编号的下标,所以还要再加1
        System.out.println(josephus(7, 3) + 1);
    }
}

3. 情况二:求全过程

3.1 思路

  解决方案是用数组模拟出局情况。大致思路如下:

  • 定义一个count变量,初始值为1,用于报数
  • 遍历数组,每访问一个新的元素,count自增,当count等于m时,则给该元素做标记,输出该元素的位置并把count置为1
  • 若当前访问的元素已做过标记,则跳过该元素,继续访问下一个元素
  • 重复上述的第2步和第3步,直至数组的所有元素都做过标记

3.2 Java代码

public class Test02 {
    /**
     * 约瑟夫环问题,求全过程
     * @param n 总人数
     * @param m 报该数的人出局
     */
    public static void josephus(int n, int m) {
        int[] nums = new int[n + 1];
        int count = 1;
        int flag = 0;
        for (int i = 0; i < nums.length; i++) {
            nums[i] = i;
            flag += i;
        }
        while (flag > 0) {
            for (int i = 1; i <= n; i++) {
                if (nums[i] == 0) {
                    continue;
                }
                nums[i] = count;
                count++;
                if (nums[i] == m) {
                    System.out.printf(i + " ");
                    count = 1;
                    nums[i] = 0;    // 对该元素做标记,下次遍历时跳过该元素
                    flag -= i;
                }
            }
        }
    }

    public static void main(String[] args) {
        josephus(7,3);
    }
}

4. 结束语

  感谢您的阅读,个人认为约瑟夫环问题的一些基本情况已全部罗列出来了,需要注意的是写代码时编号的换算。笔者能力有限,代码也未必是最优的,还请各位多多包涵!