从1开始学Java数据结构与算法——用单向环形链表解决Josephu问题
Josephu问题
设编号为1、2…n的n个人围坐一圈,约定编号为k(1<=k<=n)的人开始报数,数到第m个人出列,然后从他的下一位又开始报数,数到第m个人出列,直到所有人出列位为止,由此产生一个出队编号序列。
解决方法:
采用循环链表,如下图解:
假设有8个人,从3号开始包数,每次数4个就出列,那么如上图,第一个出列的就是6号,接着从七号开始继续数四个,那么就是2号出列。
依此类推:出列顺序就是6、2、7、4、3、5、1、8
创建单项环形链表的思路分析:
1.创建一个节点类和一个链表类
2.接着在链表类中创建一个节点,让一个节点变量first指向该节点,并独立成环,也就是令first.next = first(这里的first始终指向环形链表的第一个节点)
3.后面每当我们创建一个节点,就将节点加入到已有的环形链表中即可
4.在链表类中编写一个具体的方法去解决Josephu问题
详细思路分析:
1.遍历:
因为first需要始终指向环形链表的第一个节点,所以我们需要一个辅助节点变量curBoy来进行遍历,并初始化指向first(表示从第一个节点开始遍历),接着进行while遍历即可,结束标志就是curBoy.next == first
4.加入节点
先利用辅助节点变量curBoy遍历后指到要加入位置的前一个节点,接着执行如下三步:
curBoy.next = 新加入的节点(如下左图中的①号线生成,同时②号线解除)
新加入节点.next = first(这里其实就是默认为直接加入到最后一个位置)(如下左图中的③号线生成)
curBoy = 新加入的节点(这里千万注意,一定要让辅助节点变量后移,始终指向最后一个节点)
5.删除节点(节点出列):
1.根据用户的输入条件(输入从第n个孩子开始报数,每次数m个数),开始让节点逐个出队
2.这里注意,因为每次有一个节点出列之后,下一个节点又作为第一个节点开始报数,所以该情况下first节点变量是可以动的。但是因为为单向的环形链表,所以我们除了需要通过first先移动到出列的节点,还需要一个辅助节点helper变量,始终指向first的前一个节点。接着执行如下三步:
先让first和helper节点变量都移动(n-1)次[为了找到第一个开始报数的孩子]
再让first和helper都移动(m-1)次[为了找到每次要出列的孩子]
接着执行出列:first = first.next;helper.next = first
结果如图
3.结束标志:当该单向环形队列只剩一个节点的时候可以停止(help == first)
具体代码实现
首先肯定需要一个表示节点信息的类,这里就只包含了一个编号和next域
/**
* 节点类
* @author centuowang
* @param
* no:节点编号
* next:next域,指向下一个节点
*/
class Boy {
//这里因为类变量是私有的,所以用get和set方法来对变量进行存取
private int no;
private Boy next;
//构造函数
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
}
接着就可以开始编写我们的链表类了,在里面编写具体的解决Josephu问题的方法
/**
* 单项环形链表类
* @author centuowang
* @param
* first:始终指向第一个节点
* @method
* createLinkList(int num):构造链表方法,这里直接传入需要构造的单向环形链表的节点个数,直接循环生成链表
* show():遍历显示链表
* josephu(int start, int count, int num):出列方法
*/
class CircleLinklist{
//因为在多个方法中都需要该first变量,且都始终表示指向第一个节点,所以放在成员变量的位置
private Boy first = null;
//构造链表方法,这里直接传入需要构造的单向环形链表的节点个数,直接循环生成链表
public void createLinkList(int num) {
//先对传入的参数做个简单的校验
if(num <1) {
System.out.println("传入的节点个数不正确");
return;
}
Boy curBoy = null;//创建辅助变量
for(int i = 1; i <= num; i++) {
Boy boy = new Boy(i);
//如果是第一个节点
if(i == 1) {
first = boy;//让first指向第一个节点
boy.setNext(boy);//单个节点强行成环
curBoy = first;//辅助变量初始化,也指向第一个节点
}else {
//不是第一个节点
curBoy.setNext(boy);
boy.setNext(first);
curBoy = boy;//注意辅助变量的后移
}
}
}
//遍历显示
public void show() {
//先判断链表是否为空
if(first == null) {
System.out.println("该链表为空");
}else {
//创建辅助变量,并初始化指向第一个节点
Boy curBoy = first;
//开始循环遍历输出
while(true) {
System.out.printf("小孩的编号为:%d\n",curBoy.getNo());
if(curBoy.getNext() != first) {
curBoy = curBoy.getNext();//后移
}else {
break;
}
}
}
}
/**
* 计算出列函数
* @param
* start:从第几个节点开始报数
* count:每次报多少个数
* num:最初有多少个小孩在里面
*/
public void josephu(int start, int count, int num) {
if(first == null || start<1 || start > num) {
//对开始的位置做个简单的数据校验
System.out.println("参数输入不正确,请重新输入");
return;
}
//创建辅助变量
Boy helper = first;
//因为first一开始指向第一个节点,所以先让该辅助变量指向最后一个节点
while(true) {
if(helper.getNext() == first) {
break;
}
//后移
helper = helper.getNext();
}
//让两个节点变量移动到开始报数的节点位置,和它的前一个位置
for(int i=1; i<start; i++) {
first = first.getNext();
helper = helper.getNext();
}
//开始执行报数出列的循环过程
while(true) {
if(helper == first) {
//如果只剩下一个节点了,就停止
break;
}
//开始执行报数,让两个节点变量往后移count个位置,但实际只需要移动count-1次即可
for(int j = 1; j < count; j++) {
first = first.getNext();
helper = helper.getNext();
}
//开始执行出列
System.out.printf("第%d个小孩出列\n", first.getNo());
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留下的小孩的编号为:%d\n", first.getNo());
}
}
小问题:
在链表类中具体编写的时候,我们会发现createLinkList(int num)方法的参数num与josephu(int start, int count, int num)的第三个参数num必须一致,都是用来确定该单向环形链表中有几个节点,但是这个地方我们的代码就写的不是那么好,在两个个方法中有着一样的值得参数,如果将他抽离出来可能会更好,发生错误的概率就会小一些