用java实现单向环形链表解决约瑟夫问题

约瑟夫问题:

约瑟夫问题(有时也称为约瑟夫斯置换,是一个计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。又称“丢手绢问题”.)
其一般形式:约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3。

问题来历:

据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。 [1]
17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。

以上资料均来自------约瑟夫问题_百度百科

我们在这里把问题简化成:N个孩子围成一圈,从第k个孩子开始报数,每次从一开始报数,报到M的孩子出列,下一个孩子继续从1开始报数,直到剩下最后一个孩子,在此期间,我们要打印出出队的孩子的编号。
我们可以用单向循环链表利用数组,对数组下标进行取模运算来解决此问题,在这里我们只用单向循环链表来解决此问题。

解题思路:

假设此时N=5,k=1,M=2

  1. 按照孩子的编号创建一个没有头节点的单向循环链表,first代表的是第一个报数的孩子
  2. 将first指向的指定的第一个报数的孩子(first = first.next执行k-1次,注意不是执行k次,这是因为第一个孩子本身也要报数),在我们假定的情况下,first不需要移动。
  3. 创建一个辅助指针helper,事先让这个指针指向最后一个节点(判断条件:helper==first)
  1. 当小孩报数时,helper和first同时移动M-1次

,这时候,first所指向的孩子就是要出列的孩子

  1. 将first所指向的孩子的编号进行打印
  2. 出列的操作:实际上就是循环队列的删除节点的操作:
    first = first.next; helper.next = first

附上源码:

定义孩子节点:

class Boy {
	private int number;//孩子的编号
	private Boy next;//next是指向下一个孩子
	
	public Boy() {}
	public Boy(int number) {
		this.number = number;
	}
	
	public void setNumber(int number) {
		this.number = number;
	}
	public int getNumber() {
		return number;
	}
	
	public void setNext(Boy next) {
		this.next = next;
	}
	public Boy getNext() {
		return next;
	}
}

创建循环单链表类,并在里面定义创建没有头节点的循环单链表的方法,和打印出列的孩子的方法,和遍历整条循环单链表的方法:

创建循环单链表:

	private Boy first = null;//first是CircleSingleLinkedList类的私有属性,代表的是第一个报数的孩子
	public void addBoy(int sum) {
		if(sum<2) {//对sum进行数据校验
			throw new RuntimeException("输入的数据不符合逻辑!");
		}
		Boy curBoy = null;//curBoy是一个辅助指针,用于指向当下的循环链表中的最后一个节点,这有点像用尾插法创建单链表中的始终指向链表中最后一个节点的tail尾指针
		for(int i=1; i<=sum; i++) {
			Boy boy = new Boy(i);//创建新的孩子节点
			if(i==1) {//当循环链表只有一个孩子节点时,其next域要指向自己
				first = boy;
				first.setNext(first);
				curBoy = boy;
			}else {//当循环单链表中含有不止一个孩子节点:
				curBoy.setNext(boy);
				curBoy = boy;
				curBoy.setNext(first);
			}
		}
	}

遍历循环单链表:

	public void showBoy() {
		if(first==null) {
			System.out.println("此循环链表为空!不能进行遍历操作!!");
			return;
		}else {
			Boy cur = first;
			/*本人一开始的错误方法: 这种方法遍历的小孩总数总是会比所有小孩总数少一
			while(cur.getNext()!=first) {
				System.out.println("小孩的编号为:"+cur.getNumber());
				cur = cur.getNext();
			}*/
			
			while(true) {
				System.out.println("小孩的编号为:"+cur.getNumber());
				if(cur.getNext()==first) {
					break;
				}
				cur = cur.getNext();
			}
		}
	}

根据用户的输入,计算出小孩出圈的顺序:

startNumber表示从编号为startNumber的孩子开始报数, countNumber表示从1一直报到countNumber, sum表示孩子的总数

	public void countBoy(int startNumber, int countNumber, int sum) {
		addBoy(sum);//创建循环单链表
		if(first==null || startNumber<1 || startNumber>sum) {//先对传入的实参进行校验
			throw new RuntimeException("输入的数据有误!!");
		}
		
		//把first指针指向第一个报数的孩子
		for(int i=1; i<startNumber; i++) {
			first = first.getNext();
		}
		
		//创建辅助指针,并让辅助指针指向循环单链表的最后一个孩子节点
		Boy helper = first;
		while(helper.getNext()!=first) {
			helper = helper.getNext();
		}
		
		while(true) {
			if(first==helper) {//此时只剩下了一个孩子节点
				break;
			}else {
				for(int i=1; i<countNumber; i++) {//对first和helper的移位操作
					first = first.getNext(); 
					helper = helper.getNext();
				}
				System.out.println("出圈的小孩的编号为:"+first.getNumber());
				first = first.getNext();//删除孩子节点操作
				helper.setNext(first);;
			}
		}
		System.out.println("最后留下的小孩编号为:" + first.getNumber());	
	}

最后就是程序的运行啦:

public class Josepfu {
	public static void main(String[] args) {
		CircleSingleLinkedList list = new CircleSingleLinkedList();
		
		list.countBoy(10, 20, 125);
	}
}
约瑟夫环问题是这样的:在一个有n个人的圈子里,从第一个人开始报数,每个人报到k后出圈,然后下一个人继续,直到最后剩下的人赢得游戏。你可以使用Java链表来模拟这个过程,每个节点代表一个人,并维护一个指向下一个节点的引用。 以下是一个简单的Java代码示例,使用单向链表实现约瑟夫环问题: ```java public class JosephusRing { static class Node { int value; Node next; public Node(int value) { this.value = value; this.next = null; } } public static Node josephusProblem(int n, int k) { if (n <= 0 || k <= 0) { throw new IllegalArgumentException("Invalid input: n and k must be positive integers"); } // 创建一个长度为n的环形链表 Node[] nodes = new Node[n]; for (int i = 0; i < n; i++) { nodes[i] = new Node(i + 1); if (i != 0) { nodes[i - 1].next = nodes[i]; } else { nodes[n - 1].next = nodes[0]; // 给最后一个节点指向前一个节点形成环 } } // 开始计数并删除节点 Node current = nodes[0]; while (nodes.length > 1) { current = current.next; current = current.next % k == 0 ? current : current.next; // 报数达到k就跳过当前节点 } return current; } public static void main(String[] args) { int n = 7; // 人数 int k = 3; // 报数步长 System.out.println(josephusProblem(n, k).value); // 输出最后幸存者的位置 } } ``` 在这个代码中,`josephusProblem`函数创建了一个环形链表,然后通过循环移除节点,直到只剩下一个。当找到的节点需要跳过k个位置时,它会直接跳到下一个节点,直到找到第一个不需要跳过的节点,这就是幸存者。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值