一、问题描述
约瑟夫( Josephu ) 问题是一个非常著名的有趣的题目。问题具体描述如下:
设编号分别为1、2、3… n 的 n 个人围坐一圈,约定编号为 k(1≤k≤n)的人从 1 开始报数,数到 m 的那个人出列。出列的人的下一位又从 1 开始报数,数到 m 的那个人继续出列。以此类推,直到所有人都出列为止,由此产生一个出队编号的序列,这个序列也就是约瑟夫问题的解。
下面将用一个动图来描述一下这个问题:
假设有 4 个人围坐一圈,约定编号为 1 的人开始报数,数到 3 的那个出列。最后产生的出队编号的序列将会是:3、2、4、1。
二、解决思路
首先要确定解决问题的核心思想:使用一个不带头结点的循环(环形)链表来处理该问题。
假设每个结点代表一个人,那么一个由 n 个结点组成的循环链表就相当于是 n 个人围成的一个圈。那么约瑟夫问题以环形链表的形式来描述就是如下情景:
首先使用 n 个结点构成一个单向循环链表,然后由第 k 个结点起从 1 开始计数,当计到 m 时,从链表中删除对应结点;接着从被删除结点的下一个结点开始从 1 计数,当计到 m 时,继续从链表中删除。依次循环往复,直到链表中的所有结点都被删除为止。
那么对于这个单向循环链表形式下的约瑟夫问题,我们如何解决呢?
我们可以引入一个辅助指针 helperNode
,这个指针总是指向待删除结点的前一个结点。为什么这个辅助指针要指向待删除结点的前一个结点,而不是指向自身呢?
因为我们的目的是要删除当前计数为 m 的结点,但是受限于单向链表的特性(如果要删除单链表的某个结点,必须要知道该结点的前一个结点),我们无法让结点自己删除自己。鉴于这个特性,我们必须要引入一个辅助指针来记录当前正在计数的结点的前一个结点,这样才能符合删除条件的结点从链表中删除。
引入这个辅助指针之后,具体的操作思路如下:
- 每一轮计数开始时,总让辅助指针
helperNode
初始指向本轮第一个计数的结点; - 从第一个计数的结点开始计数至 m,实际上是向后移动了 m-1 个结点。由于辅助指针总是指向待删除结点的前一个结点,因此让需要让辅助指针从第一个计数结点后移 m-2 个结点;
- 辅助指针移动到待删除结点的前一个结点之后,只需要让辅助指针指向待删除的结点的下一个结点即可完成删除操作;
- 依次循环往复,直至只剩最后一个结点;
- 对于环形链表判断是否只有最后一个结点,只需要判断辅助指针指向的结点是否是辅助指针指向的结点的下一个结点即可。
上面的思路可以用下面一个动图来描述:
三、代码实现
此处演示代码使用的结点类是 HeroNode
:
public class HeroNode {
private int no; // 本节点数据
private String name;
private String nickName;
public HeroNode next; // 指向下一个节点
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
// getter and setter and toString
}
约瑟夫问题求解过程如下:
/**
* @Description 单向环形链表解决约瑟夫问题
* @Author Ronz
*/
public class No7_Josepfu {