问题描述
约瑟夫环(Josephus Problem)是一个经典的数学问题:有 n 个人围成一圈,从第一个人开始报数,数到 m 的人出列,然后从下一个人重新开始报数,直到所有人出列。求出列顺序,或求最后幸存者的编号。
这个问题历史悠久,传说由犹太历史学家约瑟夫提出。它在计算机科学中常用于讲解递归、递推和循环链表等数据结构。
算法思路
方法一:数学递推公式(O(n))
设 f(n, m) 表示 n 个人、步长为 m 时最后幸存者的编号(0-based)。
递推关系:
f(1, m) = 0
f(n, m) = (f(n-1, m) + m) % n
推导过程:第一次淘汰的是 (m-1) % n,剩下的 n-1 个人构成一个子问题。子问题的解 f(n-1, m) 是在新编号体系下的答案,映射回原编号需要加上 m 并对 n 取模。
方法二:循环链表模拟(O(nm))
使用循环链表模拟报数过程,逐个删除节点,直观但时间复杂度较高,适合理解问题本质。
Java实现
递推公式解法
public class JosephusMath {
/**
* 使用递推公式求解最后幸存者的编号(0-based)
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*
* @param n 总人数
* @param m 报数步长
* @return 最后幸存者的编号(0-based)
*/
public static int lastSurvivor(int n, int m) {
if (n <= 0 || m <= 0) {
throw new IllegalArgumentException("n and m must be positive");
}
int result = 0; // f(1, m) = 0
for (int i = 2; i <= n; i++) {
result = (result + m) % i;
}
return result;
}
/**
* 使用递推公式求出列顺序(1-based)
*
* @param n 总人数
* @param m 报数步长
* @return 依次出列的人的编号(1-based)
*/
public static int[] eliminationOrder(int n, int m) {
if (n <= 0 || m <= 0) {
throw new IllegalArgumentException("n and m must be positive");
}
int[] order = new int[n];
// 使用List模拟,记录当前存活的人
java.util.ArrayList<Integer> alive = new java.util.ArrayList<>();
for (int i = 1; i <= n; i++) {
alive.add(i);
}
int index = 0;
for (int i = 0; i < n; i++) {
index = (index + m - 1) % alive.size();
order[i] = alive.remove(index);
}
return order;
}
public static void main(String[] args) {
int n = 7, m = 3;
System.out.println("总人数:" + n + ",步长:" + m);
System.out.println("最后幸存者(0-based):" + lastSurvivor(n, m));
System.out.println("最后幸存者(1-based):" + (lastSurvivor(n, m) + 1));
System.out.print("出列顺序:");
for (int num : eliminationOrder(n, m)) {
System.out.print(num + " ");
}
System.out.println();
}
}
循环链表解法
/**
* 循环链表节点
*/
class CircularNode {
int value;
CircularNode next;
CircularNode(int value) {
this.value = value;
}
}
public class JosephusLinkedList {
/**
* 使用循环链表模拟约瑟夫环
* 时间复杂度:O(n × m)
* 空间复杂度:O(n)
*
* @param n 总人数
* @param m 报数步长
* @return 依次出列的人的编号
*/
public static int[] simulate(int n, int m) {
if (n <= 0 || m <= 0) {
throw new IllegalArgumentException("n and m must be positive");
}
// 构建循环链表:1 -> 2 -> 3 -> ... -> n -> 1
CircularNode head = new CircularNode(1);
CircularNode current = head;
for (int i = 2; i <= n; i++) {
current.next = new CircularNode(i);
current = current.next;
}
current.next = head; // 形成环
int[] result = new int[n];
CircularNode prev = current; // prev指向最后一个节点,便于删除
current = head;
for (int i = 0; i < n; i++) {
// 前进m-1步到达要删除的节点
for (int j = 1; j < m; j++) {
prev = current;
current = current.next;
}
result[i] = current.value;
// 删除当前节点
prev.next = current.next;
current = current.next;
}
return result;
}
public static void main(String[] args) {
int n = 7, m = 3;
System.out.print("循环链表模拟出列顺序:");
for (int num : simulate(n, m)) {
System.out.print(num + " ");
}
System.out.println();
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 数学递推 | O(n) | O(1) | 只需最后幸存者编号,大规模数据 |
| ArrayList模拟 | O(n²) | O(n) | 需要完整出列顺序,中等规模 |
| 循环链表 | O(n × m) | O(n) | 教学演示,理解问题本质 |
扩展与应用
- 变体问题:步长
m动态变化、双向报数、多 survivors 等情况 - 实际应用:资源轮询调度(如Nginx的ip_hash)、分布式系统中的令牌环、负载均衡策略
- 数学延伸:与模运算、组合数学、递归思想密切相关
总结
约瑟夫环问题展示了同一问题可以有截然不同的解法。数学递推公式利用问题的递归结构,将时间复杂度从朴素的 O(n²) 或 O(n × m) 优化到 O(n),是算法优化的经典范例。理解递推关系的建立过程,对解决其他递归问题具有重要的启发意义。