本来第二期是要更新排序的,但是发现明天学校的算法课实验是有关约瑟夫问题的,这个问题还蛮有意思的,觉得可以加更一期,话不多说,开始!
一.什么是约瑟夫问题
已知n个人(以编号1,2.3..n分别表示)围坐在一张圆桌周围。从编号为K的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到只剩下一个人为止。
以上这个问题就是约瑟夫环,我们的目标是找到剩下的那一个人,其实这个问题很常见,某些桌游就是这样的(具体是哪个忘记了);再比如丢手帕,也是围成一个圈然后不断地传递手帕,其实这都是约瑟夫环问题;
解决这个问题的数据结构实际上就是一个单向链表,不过和普通的单向链表不一样,这个单向链表的首尾是相连的,也就是说这是一个环形链表,所以我们要先构建一个环形链表。
二.构建单向环形链表
1.Node类
首先把链表的结点定义出来,这个类有两个属性,和以前的链表一模一样,一个存编号,一个存下一个结点的引用:
package com.jtl.joseph;
/**
* @description:单向环形链表的每个结点
*/
public class Node {
private int no;
private Node next;
public Node(int no){
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
没什么好说的,给相应的set、get方法就行了。
2.构建环形链表
然后我们就可以构建环形链表了,在这里我先画个图,解释一下环形链表是长啥样的,方便后面解释分析:

所以我们就可以得到怎么构建上面这个环形链表的思路:
1.先new第一个结点,然后定义first指针指向该节点,而且要形成环形结构(也就是说即使只有一个结点,它的next不能为空,也应该为first)
2.后面每次创建一个新的结点,就把该结点加入到环形链表中就可以了,这里加入的时候要用一个辅助指针来记录first的位置,因为咱们的first是不能随便改变的
具体实现:
package com.jtl.joseph;
/**
* @description:代表单向环形链表
*/
public class CircleLinkedList {
private Node first = null;//头结点;
//addNode方法,构建一个长度为nums的环形链表
public void addNode(int nums){
if(nums < 1){
System.out.println("不能创建大小 < 1的环形链表!!!!");
return;
}
Node temp = null;//辅助指针,方便插入结点
for(int i = 1; i <= nums; i++){
Node node = new Node(i);//创建新的结点并把i的值赋值给编号
if(i == 1){
//说明这是第一个结点,就让环形链表的first指向这个结点
first = node;
first.setNext(first);//形成环状结构
temp = first;//temp记录第一个结点的位置
}else{
//如果不是第一个结点,就代表要开始插入结点了
temp.setNext(node);
node.setNext(first);
temp = node;
}
}
}
}
这里有必要解释一下else里面发生的事情,让我来打开画图板细细道来:
首先,第一个if只有创建第一个结点的时候会走,此时是这样的:

接着继续new结点,这个时候就是第二个结点(设为node2)的加入了,此时是这样的:

这就是else里面干的事情,首先先把原先temp指向的结点的next设置为新结点的引用,然后把新结点的next设置为first,此时就形成了环形链表的结构,然后记住temp需要移动位置,指向最后一个结点,那么下一次就大同小异了嘛。
3.遍历环形链表
那么得到了环形链表之后,我们需要遍历输出环形链表来看看长啥样,这个时候就需要遍历整个环形链表,其实也很简单,我们也借助一个辅助指针temp,让它不断移动,当它指向结点的next值为first的时候,不久遍历到最后一个结点了吗?
//遍历环形链表
public void printAllNode(){
if(first == null){
System.out.println("此环形链表没有任何结点,无法输出!!!!");
return;
}
Node temp = first;
while(true){
System.out.printf("结点的编号值%d \n",temp.getNo());
if(temp.getNext() == first){
System.out.println("环形链表遍历完毕!!");
break;
}
//temp后移一个位置
temp = temp.getNext();
}
这个方法就可以把所有的结点拿出来并输出,首先进行判断,没有结点直接不玩了,然后设置辅助指针为temp指向first,然后在一个死循环中遍历输出每一个结点的编号,循环结束的条件是当temp的下一个位置为first的时候,遍历结束。
4.求出最后剩下的那个人
有了前面的铺垫,我们就可以开始真正解决这个问题了,我们再来看看这个问题:
已知n个人(以编号1,2.3..n分别表示)围坐在一张圆桌周围。从编号为K的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到只剩下一个人为止。
我们可以写一个方法来解决,这个方法可以求出最后一个人是谁,而且还可以把出列的顺序给拿到,题目给了n个人,然后还给了从第k个位置报数,每次数m下,这三个就是这个方法的参数:
我们还是来分析一下:

我们还是定义一个辅助指针,它的位置指向环形链表的最后一个位置(这里是5),然后我们假设从编号为1的这个人开始报数,数两下,(1)代表数的第一下,(2)代表数的第二下,此时first的位置就到2了,刚好是要出列的人的位置,然后temp同步移动,始终在fist后一个位置
那么这样做的意义是什么?其实就是要处理出列的操作:
first = first.next;
temp.next = first;
上面两句就完成了编号为2这个结点的出列,图示如下:
![]()
出列操作的图示 此时2这个结点就被回收了,等同与出列,新的环形链表将不再有编号为2的元素
具体实现:
public void countNode(int startNo,int countNum, int total){
if(first == null || startNo < 1 || startNo > total){
System.out.println("参数错误!!!!");
return;
}
Node temp = first;
while(true){
//把temp设置为最后一个结点的引用
if(temp.getNext() == first){
break;
}
temp = temp.getNext();//不断后移
}
for(int i = 0; i < startNo - 1; i++){
//如果从5开始报数,那就把first和temp移动4个位置,分别指向5这个结点和它的前一个位置
//先让first和temp移动到起始报数的那个位置
first = first.getNext();
temp = temp.getNext();
}
while(true){
if(temp == first){
break;//说明只有一个结点了
}
for(int i = 0; i < countNum - 1; i++){
//报数2次,移动1下,因为自己还要叫一下数
first = first.getNext();
temp = temp.getNext();
}
System.out.printf("结点%d出列\n",first.getNo());
//具体的出列操作:first指向下一个位置,前一个位置又指向这个位置
first = first.getNext();
temp.setNext(first);
}
System.out.printf("最后剩下来的结点编号为:%d\n" + first.getNo());
}
最终测试结果,应该是没问题的:
三.总结
这个问题的难点在于如何构建这个环形链表,其他的其实没什么,主要就是指针的移动还有指向问题需要注意,要记住什么不能随便变,如果想要借助这个不能变的东西做一些事情,就得用到一个临时变量或者辅助变量temp去解决;