据说著名犹太历史学家Josephus有过以下的故事:
在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。
然而Josephus 和他的朋友并不想遵从。
首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。
问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
这个问题被世人们称作为约瑟夫问题,这个圈也可以称作为约瑟夫环!
我们也可以将这个问题和小时候玩过的丢手绢联想起来,一群小朋友围成一个圈,约定其中一个小朋友从1开始报数,然后后面的小朋友按顺序报数,当一个小朋友报数为k时,则出列,他的下一位又从1开始报数,直到所有人都出列为止~
对于这个问题:我们需要使用一个不带头节点的单向环形链表来处理,先构成一个有n个结点的单向循环链表,然后从k结点起,开始从1计数,计数到m时,对应结点从链表中删除,被删除结点的下一个结点又从1开始计数,直到最后一个结点也被删除后,算法结束!
单向环形链表的具体实现思路
- 首先,创建一个first结点指向第一个结点,并且这个first结点不能移动!然后再创建一个辅助结点pNode,同样指向第一个结点!而且,即使只有一个结点,我们也可以让其形成环状结构,也就是将第一个结点的next域指向自己!
- 当第二个结点加入进链表的时候,我们将第一个结点的next域指向第二个结点,也就是将pNode.next指向第二个结点,然后再将第二个结点的next域指向first结点,最后将pNode结点下移!依次类推,我们就可以得到一个环形的单向链表!
// 添加结点
public void addNode(int number) {
if (number < 2) {
System.out.println("至少需要两个结点!");
return;
}
Node pNode = null;
for (int i = 1; i <= number; i++) {
Node node = new Node(i);
if (i == 1) {
first = node;
first.next = first;
pNode = node;
} else {
pNode.next = node;
node.next = first;
pNode = node;
}
}
}
// 遍历结点
public void display() {
if (first == null) {
System.out.println("该链表为空!");
return;
}
Node pNode = first;
while(true) {
System.out.println(pNode);
if (pNode.next == first) {
break;
}
pNode = pNode.next;
}
}
}
约瑟夫问题的具体实现
假设链表中一共有5个结点,我们从第1个结点开始计数,当计数到2时,对应结点从链表中删除,下一个结点继续从1开始计数,直到剩下最后一个结点!简单的演算,我们可以得到删除的顺序为2->4->1->5->3。
具体实现以上所述的操作,我们需要创建一个辅助结点helper,事先指向first结点的前一个结点,假设此时first指向第一个结点,那么helper就指向最后一个结点!
但在问题的描述中,我们可以定义从第k个结点开始计数,于是我们先将first结点和helper结点移动k-1次!
- 设置一个变量count存储规定的计数,当计数为count时,对应结点从链表中删除,同时,first结点和helper结点也需要移动count-1次,这样first正好指向待删除的结点,而help指向前一个结点。
- 此时,开始进行结点删除操作!首先将first结点向下移动一次,即first=first.next,然后再将helper.next指向first!
// 约瑟夫
public void Josephus(int startNo, int count) {
if (startNo < 1 || first == null || count < 0 || count > size) {
System.out.println("参数输入有误!");
return;
}
Node helper = first;
// 将helper和first移动至第一个计数的结点
for (int i = 0; i < startNo - 1; i++) {
helper = helper.next;
first = first.next;
}
// 将helper移动到first的前一个结点
while(true) {
if (helper.next == first) {
break;
}
helper = helper.next;
}
while(true) {
if (helper == first) {
break;
}
// 开始计数,将first移动至删除结点
for (int i = 0; i < count - 1; i++) {
first = first.next;
helper = helper.next;
}
System.out.println(first);
first = first.next;
helper.next = first;
}
System.out.println("最后的结点为:" + first);
}
}