【算法】约瑟夫环问题

约瑟夫环(Josephus Problem)

问题描述

约瑟夫环是一个经典的数学与计算机问题:

n 个人围成一圈,从第 1 个人开始报数,报到 m 的人出列;然后从出列者的下一个人重新开始报数,报到 m 的人再出列......如此循环,直到所有人都出列。求最后剩下的那个人(或求出列顺序)。

这个问题源自历史传说:约瑟夫和他的同伴被罗马人围困,他们决定自杀而不被俘,于是围成一圈,每数到第 3 个人就杀掉,约瑟夫通过计算找到了存活的位置。


一、模拟法(链表 / 数组模拟)

最直观的思路:用一个循环链表(或数组标记)模拟整个过程。

  • 时间复杂度:O(n·m)
  • 空间复杂度:O(n)
  • 优点:能直接得到完整的出列顺序,易于理解
Java 代码(数组模拟)
java 复制代码
import java.util.ArrayList;
import java.util.List;

public class JosephusSimulation {

    /**
     * 模拟法求约瑟夫环出列顺序
     * @param n 总人数(编号 1 ~ n)
     * @param m 报数到 m 出列
     * @return 出列顺序列表
     */
    public static List<Integer> josephusOrder(int n, int m) {
        List<Integer> result = new ArrayList<>();
        boolean[] out = new boolean[n + 1]; // out[i] = true 表示第 i 人已出列
        int count = 0;   // 已出列人数
        int index = 0;   // 当前报数位置(0 表示在 1 号之前)
        int call = 0;    // 当前报数

        while (count < n) {
            index = index % n + 1; // 环形移动到下一个人(1~n)
            if (!out[index]) {
                call++;
                if (call == m) {
                    out[index] = true;
                    result.add(index);
                    count++;
                    call = 0;
                }
            }
        }
        return result;
    }

    public static void main(String[] args) {
        int n = 7, m = 3;
        List<Integer> order = josephusOrder(n, m);
        System.out.println("出列顺序: " + order);
        System.out.println("最后剩下: " + order.get(order.size() - 1));
    }
}
输出
复制代码
出列顺序: [3, 6, 2, 7, 5, 1, 4]
最后剩下: 4

二、数学递推法(最优解)

如果只要求最后剩下的那个人的编号,可以用递推公式,时间复杂度 O(n),空间 O(1)。

递推思想

设 f(n, m) 表示 n 个人(编号 0 ~ n-1)报数到 m 时最后剩下的人的编号。

  • 第一个人出列后,剩下 n-1 人,问题规模缩小。
  • 出列者下标为 (m-1) % n,从它下一位重新编号,可得到递推关系:

f(1,m)=0 f(1, m) = 0 f(1,m)=0

f(n,m)=(f(n−1,m)+m) mod n f(n, m) = \big(f(n-1, m) + m\big) \bmod n f(n,m)=(f(n−1,m)+m)modn

最终结果 f(n, m) 是 0 基编号,若题目要求 1 基编号,加 1 即可。

Java 代码(迭代实现)
java 复制代码
public class JosephusMath {

    /**
     * 数学递推法求最后剩下的人(1 基编号)
     * @param n 总人数
     * @param m 报数到 m 出列
     * @return 最后剩下的人的编号(1 ~ n)
     */
    public static int lastRemaining(int n, int m) {
        int ans = 0; // f(1, m) = 0,0 基编号
        for (int i = 2; i <= n; i++) {
            ans = (ans + m) % i;
        }
        return ans + 1; // 转成 1 基编号
    }

    public static void main(String[] args) {
        int n = 7, m = 3;
        System.out.println("最后剩下: " + lastRemaining(n, m)); // 4
    }
}
递归写法(便于理解)
java 复制代码
public static int f(int n, int m) {
    if (n == 1) return 0;
    return (f(n - 1, m) + m) % n;
}
// 结果 = f(n, m) + 1

⚠️ 注意:n 很大时递归会栈溢出,建议用迭代。


三、两种方法对比

方法 时间复杂度 空间复杂度 能否得到出列顺序
模拟法 O(n·m) O(n) ✅ 可以
数学递推 O(n) O(1) ❌ 只能得最后一人

四、常见变体与注意事项

  1. 编号从 0 还是 1 开始:数学公式默认 0 基,输出时按需 +1。
  2. m = 1 的情况:每次都是当前人出列,最后剩下的是第 n 个人。
  3. n = 1:直接返回该人。
  4. 求第 k 个出列的人:可用模拟法;若 n 很大可用更高级的"跳跃删除"优化(如使用线段树 / 有序集合,O(n log n))。
  5. 大 n 优化 :当 m 远小于 n 时,数学递推中 ans + m 可能连续多步不取模,可批量跳跃优化到接近 O(m log n) 或 O(n/m) 级别。

五、示例验证

n = 7, m = 3 时手动推演:

复制代码
初始: 1 2 3 4 5 6 7
报3出列: 3        → 剩 1 2 4 5 6 7
从4报:   6        → 剩 1 2 4 5 7
从7报:   2        → 剩 1 4 5 7
从4报:   7        → 剩 1 4 5
从1报:   5        → 剩 1 4
从1报:   1        → 剩 4
最后剩下 4 ✅

与两种算法的输出一致。

相关推荐
2601_962106641 小时前
【Java Set综合实战】商品管理系统 + 图书收藏系统|Product、Book自定义类全套代码
java
RuoyiOffice2 小时前
SpringBoot+Vue3 凭证生命周期设计:审核、过账、冲销与反过账怎样保持账目一致
java·spring boot·事务·springboot3·财务系统·会计凭证·ruoyi office
GoAI2 小时前
DeepSeek Harness Memory (一)主流记忆插件总结
后端·算法·面试
2601_962106642 小时前
HashMap入门|工号-姓名键值存储、两种遍历方式、根据key/value查
java
Pniubi3 小时前
力扣54.螺旋矩阵
算法·leetcode·矩阵
杨凯凡3 小时前
Spring Framework 7 API 版本管理实战:请求头路由、兼容迁移与废弃策略
java·rest api·springframework·spring boot 4·api版本管理
T_Wizard3 小时前
同一个问题写了 4 遍:官方 SDK / HTTP / Spring AI / LangChain4j,到底该选哪个
java·ai编程
hai_android3 小时前
AudioTrack 深度解析:从原理到实战
android·java·kotlin
斯维赤4 小时前
限流、熔断、降级,你真的分得清吗?
java·后端·架构