【算法】约瑟夫环问题

约瑟夫环(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 ✅

与两种算法的输出一致。

相关推荐
郝亚军44 分钟前
manifest 中 `sha256` 与 `signValue` 的作用
java·开发语言·数据结构
l1t1 小时前
测试DuckDB v2.1.0-alpha49002的bignum乘法
数据库·sql·算法·duckdb
譕痕1 小时前
stripe 实例
java·开发语言·stripe·checksession
Nil2081 小时前
LeetCode 283移动零
数据结构·算法·leetcode
墨子夕1 小时前
设计模式——Java × Python 双语言精讲 · 23 个经典模式 + 现代演进(前言篇)
java·开发语言·设计模式
骇客野人1 小时前
Linux 全套实操方案(含目录详解、文件传输、日志、文件规范、环境变量、Java 应用部署运维)
java·linux·运维
CQU_JIAKE1 小时前
10.18【A】
算法
.Bryce.2 小时前
MCP+A2A+ANP,Agent通信协议的完整知识地图
java·人工智能·后端·agent·通信协议
SL_staff2 小时前
规则引擎选型陷阱:为什么QPS不是JVS-Rules对比Drools的核心指标
java·开源·全栈