简介
最早碰到这个问题是在读大学刚开始学数据结构的时候。还记得当年为了验证自己的一种思路连续调试了好几天,最后虽然得出了一个结果,不过算法的时间复杂度达到了O(n^3)。现在回顾起来挺有意思的。
问题分析
Josephus环的问题看起来很简单,假设有n个人排成一个圈。从第一个人开始报数,数到第m个人的时候这个人从队列里出列。然后继续在环里数后面第m个人,让其出列直到所有人都出列。求所有这些人出列的排列顺序。
一个典型的示例如下图所示:
在上图中,我们从n1元素开始顺时针数到第4个元素,然后n4号出列。这样,我们就剩下了7个元素。我们在剩下的元素里按照原来顺序继续数到后面4个。这样一直下去,我们可以看到依次找到的出列元素为n4,n8,n5,n2,n1,n3,n7,n6。
解法一: 队列
一种方法是我们可以使用队列。怎么来处理呢?因为我们每次都是处理n个元素里第m个元素。如果我们每次从队列里一边取元素,一边又加入到队列的末尾,直到数到第m的时候。这个第m的元素直接让它移除,我们就保证了取到恰当的元素,同时又保证原来环的顺序没有改变。这样一直循环n遍,我们就可以将所有元素都取出来了。从前面讨论的过程我们就可以看到,它的时间复杂度为O(m*n)。
一个参考的代码实现如下:
import java.util.Queue; import java.util.ArrayDeque; public class Josephus<T> { private Queue<T> queue; public Josephus(int length) { if(length <= 0) throw new IllegalArgumentException("Invalid length!"); queue = new ArrayDeque<T>(length); } public void process(int interval) { if(interval <= 0) throw new IllegalArgumentException("Invalid interval"); int length = queue.size(); for(int i = 0; i < length; i++) { for(int j = 0; j < interval; j++) { T t = queue.remove(); queue.add(t); } T removed = queue.remove(); System.out.println(removed); } } public void add(T t) { queue.add(t); } public static void main(String[] args) { Josephus<Integer> josephus = new Josephus<Integer>(7); josephus.add(1); josephus.add(2); josephus.add(3); josephus.add(4); josephus.add(5); josephus.add(6); josephus.add(7); josephus.process(3); } }
这里的方法借用了jdk里默认自带的队列。算是稍微取了一点巧。
解法二:循环链表
这一种思路和前面的很近似,就是使用一个循环链表,然后每次数到给定的数字m时删除这个指定的元素。在jdk里的LinkedList就是一个这样的典型数据结构。整体的过程伪代码实现如下:
public static void process(LinkedList list, int m, int n) { Node node = list.first; for(int i = 0; i < n; i++) { for(int j = 0; j < m; j++) { node = node.next; } System.out.println(node); list.remove(node); } }
另外一种思路
前面那两种思路看起来比较简单直接,可是从另外一个角度来看觉得似乎思考的深度不够。既然是一个n人的环,然后每次到第m个的时候就去掉。这样的数学过程是不是有一个数学层面的规律可循呢?如果这样的问题可以通过一个简单的数学公式就可以解决的话,那岂不是更好?让我们先将问题稍微简化一点。假定我们不考虑他们顺序移除的元素,就考虑移除某一个元素之后他们之间的对应关系。
我们来看下图:
对于一个长度为7的环,我们走的步长是4。在走过4步之后我们找到3这个元素,并将它出队。然后我们在3后面的元素,4开始继续下一个查找步骤。而实际上我们从这个时候开始,不正是从n-1个元素里开始取元素了吗?因此我们可以将这个下一步取元素的问题归结为从n-1个元素里取下一个。不过,在上面的示例中,我们是在走到应该为4的元素那里重新以元素0开始作为n-1个元素取下一个的基础。因此,他们之间还存在着一个转换的关系。
我们再从一个更加一般的场景来考虑。在第一个人出队之后,这个第一个出队的人的编号必然为(m - 1) % n。剩下的n-1个人组成一个新的Josephus环。只是这个时候我们是以m % n开始。假定k = m % n。他们组成一个这样的序列:
k, k+1, k+2...n-2, n-1, 0, 1, ... k-2。这个序列中缺少的k-1恰好就是我们前面一次遍历的时候找到并移除的。在我们将他们归结为n-1规模的Josephus环时,我们对他们有了这么一个映射:
k --> 0
k+1 --> 1
k+2 --> 2
...
...
k-3 --> n-3
k-2 --> n-2
这说明了一个什么问题呢?这说明对于我们在n-1的环中,任何一个元素的index对应到n的环中时他们之间差了k,也就是m % n。而这里的差不是一个简单的小于,而是由于整个环的结构,相当于一个循环进位的效果。这样,既然我们在n - 1对应到n的环中间是差了m % n,在更加一般的情况下,任何一个长度为l的环的元素对应到l +1的环的index都是差了这么个m % l。
现在到了问题的关键点了。我们在一个n长的环里取m的步长,然后这个环里少了一个。剩下的n-1个元素构成了n-1环。而这里的元素和n长的元素之间的映射关系是Index(n) = (Index(n - 1) + m) % n。而如果我们载往下一步移除元素呢,他们之间的关系则是Index(n - 1) = (Index(n - 2) + m) % (n - 1)。哈哈,有意思,我们好像找到点规律了。没错,按照刚才的过程,我们这样一直移除元素下去,肯定能够找到最后一个被移除的元素。这个元素则对应只有一个元素的环,很显然,它的值为0。也就是Index(1) = 0。对于这个元素的索引,它对应两个元素的索引是多少呢?按照前面的过程,我们倒推回去就是了。Index(2) = (Index(1) + m) % 2。那么对应3个,4个元素的呢?我们这样一路继续下去就可以找到对应到n个元素的索引了。所以,我们发现了一个有意思的数学归纳关系:
f(1) = 0, f(n) = (f(n - 1) + m) % n。
按照这个关系,我们可以得到最后一个被取出来的元素对应到n个元素的环里的索引值。按照这个公式,我们可以定义出如下的代码:
public static void simulate(int n, int m) { int answer = 0; for(int i = 1; i <= n; i++) { answer = (answer + m) % i; System.out.println("Survival: " + answer); } }
运行这段代码的输出如下:
Survival: 0 Survival: 1 Survival: 1 Survival: 0 Survival: 3 Survival: 0 Survival: 3
这里最有意思的就是里面输出的每个数字都是对应到不同长度的索引值。 比如这里我们对应的7个元素里,最后一个被选择到的在索引为3的那个位置。这就是数学的力量啊,真美!
总结
Josephus环问题是一个很老的问题了。从10多年前碰到它,自己用一种很笨拙的方式去解决它,到现在考虑的用队列和循环链表解决,以及考虑相关的数学关系。我们可以发现一些看似简单的问题其实蕴含着很深层次的数学之美。在一些元素位置的推导方面目前自己还有一些地方理解的不够完善,后续还会继续补充说明。
参考材料
http://comicmimiboy.blog.163.com/blog/static/1511582702011729102428974/
相关推荐
约瑟夫环,又称约瑟夫问题(Josephus Problem),源自一个古老的传说,它在计算机科学中被用来研究循环链表、递归算法以及组合数学等领域。在这个实习报告中,我们将围绕约瑟夫环的理论、实现和应用进行详尽的探讨。...
标题“Josephus”所指的,是著名的约瑟夫环问题(Josephus Problem),这是一个在计算机科学和数学中常见的理论问题。这个问题源自一个历史故事,讲述了在罗马围攻犹太城时,一群犹太人为了避难而形成一个圈,然后...
约瑟夫环约瑟夫环(Josephus Problem),又称为约瑟夫斯置换,是一个在计算机科学和数学中广泛讨论的问题。其起源于一个历史故事:在罗马人占领乔塔帕特后,约瑟夫和他的朋友与39个犹太人躲到一个洞中,他们决定以...
Java编程语言在教育领域被广泛用于教授计算机科学基础,其中一个经典的算法问题就是约瑟夫环(Josephus Problem)。这个题目源自一个古老的故事,涉及到在战争中存活下来的策略。在这个问题中,人们站成一个圈,并...
标题中的“C语言编写的关于约瑟夫环问题的程序”指的是使用C编程语言实现的一个经典算法问题——约瑟夫环(Josephus Problem)。约瑟夫环问题是一个理论上的问题,通常在计算机科学和数学中被用作示例,以讨论和解决...
`约瑟夫环问题.doc`可能是对问题的进一步解释或程序设计的文档,而`第4组小组作业.ppt`可能是团队作业的演示文稿,包含问题的背景介绍、解决方案的详细步骤以及可能的扩展讨论,比如如何优化算法,减少时间复杂度等...
这部分信息涉及到计算机科学中的一个重要数据结构问题——约瑟夫环(Josephus Problem),以及编程环境Visual C++ 6.0的使用。以下是对这一知识点的详细阐述: ### 约瑟夫环(Josephus Problem) 约瑟夫环问题源自...
本文将重点讨论如何使用向量法来解决约瑟夫斯问题,以及在C语言中如何实现这一算法。 向量法,也称为动态数组,是处理序列数据的一种常见方法。它提供了一种方便的方式来存储和访问元素,尤其适用于需要频繁插入和...
在计算机科学中,约瑟夫环问题(Josephus Problem)是一个著名的理论问题,它源于一个古老的传说,涉及到生存策略和循环移位。此问题通常被用作数据结构课程设计中的经典实例,因为它能够有效地展示链表、队列、栈等...
约瑟夫环(Josephus Problem)是一个经典的理论问题,它在数据结构和算法的教学中常被用作实例,来展示链表、队列、栈等数据结构的应用。这个实习报告将深入探讨这个问题,并通过源代码进行实际实现。 约瑟夫环问题...
这个问题可以用多种方法来解决,这里我们将讨论三种使用Python实现的方法。 1. **基于数组概念的解法** 这种方法利用Python的列表(list)作为数组的替代品,因为Python内建的数组类型是元组(tuple),不支持动态...
在这个特定的场景中,我们利用链表来解决著名的约瑟夫环(Josephus Problem)问题。约瑟夫环问题是一个理论上的问题,其背景是古代犹太人约瑟夫在战争中为了避免被敌人俘虏,让幸存者围成一个圈,按照一定的规则每隔...
**约瑟夫环问题**是数据结构领域中的一个经典算法问题,源于公元前一世纪的犹太历史传说。问题描述如下:假设有一群人围成一个圈,从某个人开始按顺时针方向报数,每次报到特定数字的人将被排除出圈,然后下一轮从该...
在计算机科学中,约瑟夫环常被用来讨论和实现循环链表的删除操作,以及在分布式系统中解决资源争抢等问题。 这个问题的解决方案多种多样,常见的算法有基于数组和链表的实现。例如,我们可以创建一个大小为n的循环...
约瑟夫环(Josephus Problem)是一个著名的理论问题,源于公元前一世纪犹太历史学家约瑟夫·弗拉维乌斯的叙述。该问题在计算机科学中被广泛讨论,主要涉及算法设计和数据结构的应用。在约瑟夫环问题中,人们围成一个...
约瑟夫环(Josephus Problem)是一个著名的理论问题,源于公元前一世纪犹太历史学家约瑟夫·弗拉维乌斯讲述的一个故事。在问题中,人们站成一个圈,并按照某种顺序依次淘汰,最后剩下的人将获得某种奖励或者幸免于难...
约瑟夫环(Josephus Problem)是一个著名的理论问题,源于公元前一世纪犹太历史学家Flavius Josephus提出的一个故事。在古代战争中,一群士兵被迫围成一个圈,然后按照一定的规则每k个人中就有一人被杀,直到只剩...
约瑟夫环,又称为约瑟夫问题(Josephus Problem),是由数学家约瑟夫·弗兰克在1963年提出的一个理论问题。该问题的基本设定是:有一群人围成一个圆圈,从某个人开始按顺时针或逆时针顺序报数,每报到特定数值的人会...
约瑟夫环(Josephus Problem)是一个著名的理论问题,源于公元前一世纪犹太历史学家约瑟夫·弗拉维乌斯的叙述。该问题在计算机科学中被广泛讨论,主要涉及算法设计和数据结构的应用。在C语言环境下,解决约瑟夫环...