约瑟夫问题,有时也称为约瑟夫斯置换,是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。又称“丢手绢问题”.
假设有num个小孩在玩丢手绢,现在从第n个小孩开始数数,数到第m个数,把这个数删除,接着从这个被删除的数开始数m个,在删除,再数…
例如:有五个孩子在玩丢手绢,从第二个小孩开始数,数2次,那么会得到一个出圈顺序:3-5-2-1-4
解决约瑟夫问题,我们正好可以用环形链表
相比于普通的单向链表,单向环形链表是把最后一个结点又指向了第一个结点。
因此我们可以先构建一个结点类:
class Boy{
private int no;//编号
private Boy next;//指向下一个结点
public Boy(int no) {
this.no=no;
}
/**
* @return the no
*/
public int getNo() {
return no;
}
/**
* @param no the no to set
*/
public void setNo(int no) {
this.no = no;
}
/**
* @return the next
*/
public Boy getNext() {
return next;
}
/**
* @param next the next to set
*/
public void setNext(Boy next) {
this.next = next;
}
向环形链表中添加结点
- 我们首先需要定义一个指向第一个结点的next,再定义一个辅助指针curboy
- 当链表内只有一个结点时,让first指向这个结点,让这个结点自己形成环,让这个辅助指针指向这个结点
- 再加入结点时,先把curboy的next指向新的结点,接着把新的结点指向first,最后把curboy指向新的结点
//先创建一个first结点
private Boy first = null;
//添加小孩结点,构建一个环形链表
public void addBoy(int nums) {//向连表中添加nums个结点
//对nums做一个数据校验
if(nums<1) {
System.out.println("nums的值不符合情况");
return;
}
Boy curboy = null;//作为辅助指针,帮助构建环形链表
//利用循环来创建环形链表
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);//先让curboy指向新的结点,即把当前的结点与新加入的结点连接起来
boy.setNext(first);//再把新的结点指向first
curboy=boy;//把curboy后移,指向这个新的结点
}
}
}
遍历环形链表
- 因为first不能动,所以创建一个辅助指针cur
- 利用循环进行遍历,当cur.getNext()==first时,遍历完毕
//遍历环形链表
public void showboy() {
//判断链表是否为空
if(first == null) {
System.out.println("链表为空");
return;
}
//因为first不能动,因此使用一个辅助指针cur
Boy cur = first;
while(true) {
System.out.printf("小孩的编号%d\n",cur.getNo());
if(cur.getNext()==first) {//此时已经遍历完毕
break;
}
cur = cur.getNext();
}
}
得到一个出圈顺序
- 创建一个方法,传入三个参数n,m,num 其中n表示从第几个小孩开始数,m表示要数几个数,num表示共有多少个小孩
- 出圈即意味着删除结点,那么我们可以借用单链表删除结点的方法,定义一个辅助指针helper用于表示要删除结点的上一个结点。
- 初始时我们把helper指向环形链表的最后一个结点
- 在小孩报数前,我们先把helper和first移动到开始报数的位置。例如:开始报数的位置为2 那么示意图如下
其中first移动到了开始报数的第二个结点,helper移动到了该结点的前一个结点1. 他们分别移动了2-1=1次. - 接着小孩开始报数,如果需要报m个数,那么first和helper分别移动m-1次,移动相应次数后,把该结点出圈(即删除该结点),当helper==first时,说明只剩下一个结点,返回。例如:从第二个结点开始数,数2个数,即m=2.那么出圈顺序为3 5 2 1 4。
先移动first和helper到相应位置,这里从第二个数开始数,数2个数,即从2开始,数2个数后(包括自己)为3.
接着把first向后移,再把helper指向first,即代表原本first对应的结点位置没有加入链表,在java中会被当做垃圾回收。
循环这个过程即可得到一个出圈顺序
//根据用户的输入,返回一个出圈顺序
public void outcircle(int m,int n,int num) {//其中m表示要数几下,n表示从第几个小孩开始数,num表示有几个小孩
if(first == null || n<1 || n>num) {
System.out.println("参数输入有误,请重新输入");
return;
}
//创建一个辅助指针,帮助完成小孩出圈
Boy helper = first;
//先把helper指向环形链表的最后一个结点。(因为单链表要删除一个结点,需要指向这个结点的前一个结点,那么这里初始默认是最后一个结点)
while(true) {
if(helper.getNext()==first) {//说明helper指向了最后一个结点
break;
}
helper = helper.getNext();
}
//小孩报数前,先把first与helper移动到开始报数的位置,即移动n-1次
for(int i=0;i<n-1;i++) {
first=first.getNext();
helper=helper.getNext();
}
//小孩报数时,让first与helper指针同时移动m-1次,然后出圈
//一个循环操作,直到圈中只要一个结点
while(true) {
if(helper == first) {//此时说明圈中只有一个结点
break;
}
//让first与helper指针同时移动m-1次
for(int j=0;j<m-1;j++) {
first=first.getNext();//此时first指向的结点就是要出圈的小孩结点
helper=helper.getNext();
}
System.out.printf("小孩%d出圈\n",first.getNo());
//此时将first指向的小孩结点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的小孩编号是%d\n",first.getNo());
}
总源码:
package Linkedlist;
public class Josephu {
public static void main(String[] args) {
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
circleSingleLinkedList.showboy();
circleSingleLinkedList.outcircle(2, 2, 5);
}
}
//创建一个环形的单向链表
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(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);//先让curboy指向新的结点,即把当前的结点与新加入的结点连接起来
boy.setNext(first);//再把新的结点指向first
curboy=boy;//把curboy后移,指向这个新的结点
}
}
}
//遍历环形链表
public void showboy() {
//判断链表是否为空
if(first == null) {
System.out.println("链表为空");
return;
}
//因为first不能动,因此使用一个辅助指针cur
Boy cur = first;
while(true) {
System.out.printf("小孩的编号%d\n",cur.getNo());
if(cur.getNext()==first) {//此时已经遍历完毕
break;
}
cur = cur.getNext();
}
}
//根据用户的输入,返回一个出圈顺序
public void outcircle(int m,int n,int num) {//其中m表示要数几下,n表示从第几个小孩开始数,num表示有几个小孩
if(first == null || n<1 || n>num) {
System.out.println("参数输入有误,请重新输入");
return;
}
//创建一个辅助指针,帮助完成小孩出圈
Boy helper = first;
//先把helper指向环形链表的最后一个结点。(因为单链表要删除一个结点,需要指向这个结点的前一个结点,那么这里初始默认是最后一个结点)
while(true) {
if(helper.getNext()==first) {//说明helper指向了最后一个结点
break;
}
helper = helper.getNext();
}
//小孩报数前,先把first与helper移动到开始报数的位置,即移动n-1次
for(int i=0;i<n-1;i++) {
first=first.getNext();
helper=helper.getNext();
}
//小孩报数时,让first与helper指针同时移动m-1次,然后出圈
//一个循环操作,直到圈中只要一个结点
while(true) {
if(helper == first) {//此时说明圈中只有一个结点
break;
}
//让first与helper指针同时移动m-1次
for(int j=0;j<m-1;j++) {
first=first.getNext();//此时first指向的结点就是要出圈的小孩结点
helper=helper.getNext();
}
System.out.printf("小孩%d出圈\n",first.getNo());
//此时将first指向的小孩结点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的小孩编号是%d\n",first.getNo());
}
}
//先创建一个结点
class Boy{
private int no;//编号
private Boy next;//指向下一个结点
public Boy(int no) {
this.no=no;
}
/**
* @return the no
*/
public int getNo() {
return no;
}
/**
* @param no the no to set
*/
public void setNo(int no) {
this.no = no;
}
/**
* @return the next
*/
public Boy getNext() {
return next;
}
/**
* @param next the next to set
*/
public void setNext(Boy next) {
this.next = next;
}
}