约瑟夫问题描述:一共有n个人,从第k个人开始报数,数到m的人出列,它的下一位又从1开始报数,数到m又出列,以此类推,直到所有的人都出列为止,由此产生一个出队的序列;
例如一共有5个人,编号为1,2,3,4,5从第一个人开始,每数两下就出列一个人,直到最后一个人也出列为止;
①1号先开始报数,一共数了两下,到了2号,所以2号出列,序列剩下1,3,4,5;
②2号出列后,从3号开始报数,数两下,到了4号,所以4号出列,序列剩下1,3,5;
③4号出列后,从5号开始报数,数两下,到了1号,所以1号出列,序列剩下3,5;
④1号出列后,下一个节点是3号,所以从3号开始报数,数两下,到了5号,所以5号出列,序列剩下3
⑤5号出列后,序列只有3号,所以3号最后出列;
出队结束;
出队序列为:2,4,1,5,3
所以在这里可以看出来,从1号开始报数出列,序号大的元素可能会先出列,例如4号先出列了,然后再到1号,再到5号;
所以单链表在这里不适用;但是如果是一个循环链表的话,把头和尾都连接起来,这样形成一个环状,正好就比较适合约瑟夫问题的解决;
所以这里可以采用循环链表的方式来解决约瑟夫问题;
思路:
构建单向循环链表:
先创建第一个节点,让first指针指向该节点,并形成环状;
后面每当创建一个新的节点,就把该节点加入到已有的循环链表当中即可;
遍历循环链表:
先创建一个辅助指针temp,先让它指向循环链表的最后一个节点,着这样它的下一个节点就是first了;
从第k个人开始报数,所以在报数之前,先让first和temp往后移动k-1次;
每数m下就出列一个人,当开始报数的时候,让first和temp指针同时往后移动m-1次
这样first指针指向的节点就可以出列了;
然后从下一个节点开始,继续循环,依次出队;
当temp.next == first的时候,遍历结束
代码实现如下:
创建单向循环链表
//创建一个环形的单向链表
class CircleSingleLinkedList {
//先创建一个first节点,当前没有编号
private Boy first = null;
//添加小孩节点,构建一个环形的链表
public void addBoy(int nums) { //nums表示要添加的小孩节点的个数
//nums 作一个数据校验
if (nums < 1) { //环形链表当中至少要有一个小孩节点
System.out.println("nums的值不正确");
return;
}
Boy curBoy = null; //辅助指针,帮助构建环形链表
//使用for循环来创建环形链表
for (int i = 1; i <= nums; i++) {
//根据编号,创建小孩节点
Boy boy = new Boy(i);
//如果是第一个小孩节点
if (i == 1) {
first = boy;
first.setNext(first); //构成环状
curBoy = first; //让curBoy指向第一个小孩
}
else {
curBoy.setNext(boy);
boy.setNext(first);
curBoy = boy;
}
}
}
遍历循环链表
//遍历当前环形链表
public void showBoy() {
//判断链表是否为空
if (first == null) {
System.out.println("没有任何小孩");
return;
}
//因为first不能动,因此需要一个辅助指针来完成遍历
Boy curBoy = first;
while (true) {
System.out.printf("小孩的编号为 %d \n",curBoy.getNo());
if (curBoy.getNext() == first) { //已经遍历到了链表的最后
return;
}
curBoy = curBoy.getNext(); //curBoy后移
}
}
计算出队顺序
/**
*
* @param startNo 表示从第几个小孩开始数数
* @param countNum 表示数几下
* @param nums 表示最初有几个小孩在圈中
*/
//根据用户的输入,计算出小孩的出圈顺序
public void countBoy(int startNo, int countNum, int nums) { //startNo是从第几个小孩开始,countNum是数几下,nums是小孩的数量
//先对数据进行校验
if (first == null || startNo < 1 ||startNo > nums) {
System.out.println("参数输入有误,请重新输入");
}
//创建一个辅助指针,帮助小孩完成出圈
Boy helper = first;
//先让辅助指针helper事先指向环形链表的最后这个节点
while (true) {
if (helper.getNext() == first) {
break;
}
helper = helper.getNext();
}
//小孩报数前,先让first和helper移动startNo-1次,first移动到要开始出列的节点startNo,helper指向的是startNo节点的前一个节点
for (int j = 0; j < startNo - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
//当小孩报数时,让first和helper指针同时移动 countNum-1 次,然后出圈
//这里是一个循环操作,直到圈中只有一个节点
while (true) {
if (helper == first) { //说明圈中只有一个小孩节点
break;
}
//让first和helper指针同时移动countNum-1次,然后出圈
for (int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//这时first指向的节点,就是要出圈的小孩
System.out.printf("小孩%d出圈\n",first.getNo());
//这时将first指向的小孩节点出圈
first = first.getNext();
helper.setNext(first); //让helper的next域指向first节点
}
System.out.printf("最后留在圈中的小孩编号是%d\n",first.getNo());
}