一、问题描述
N个人围成一圈,从第K个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,K=1,M=5, 被杀掉的顺序是:5,4,6,2,3,1。也就是说,最终1会活下来,编程求出出圈的顺序。
二、涉及知识点
- 单向循环链表:https://blog.csdn.net/mail_liuxing/article/details/97976000
- 数组
我们可以通过上述两种知识中的某一个来解题。
三、思路分析
-
单向循环链表LinkedList:(solveByJdkLinkedList)
使用单向循环链表的话,结构上和题目描述很像。
①. 使用LinkedList是最简单的,因为LinkedList提供了很多丰富的方法,比如:public E remove(int index) 可以删除指定下标的元素,同时返回这个位置的元素。
②. 我们只需要一直计算要被删除的元素的下标,然后通过remove方法删除即可,一直重复到链表为空就结束了。
③. 计算下标的时候,需要注意的时候,我们需要对list.size()取模来获取正确的下标:
比如现在被删除的元素位置是targetIndex,那么下一个位置应该是:(tartgetIndex + m - 1) % list .size()
④.当list.isEmpty() == true的时候,表示没有元素了,程序结束。 -
数组实现:solveByArray
其实使用数组的方式和LinkedList类似,需要不停的计算下标,不过有几个不同的地方:
①.数组没有类似于remove(i)的方法,这个我们可以自己提供一个;
②.数组的长度不会像LinkedList在删除元素之后会自动-1,所以我们需要通过一个变量来计算删除元素后数组的长度,而不能直接使用length方法;
③.对于结束条件来说,不能像LinkedList一样直接使用isEmpty这种方法,我们可以通过判断出圈的人个数count是否等于n来结束循环。 -
数组优化实现(solveByArrayefficient)
上面的方式,有一个不好的地方,就是需要不同删除元素(通过移动元素实现),效率较低,可以优化如下:
①. 元素的删除,不再真正的删除,而是把对应位置给上一个标志值如0来表示
②. 因为元素不删除,所以每次计算的下标应该是真实的没有移动的下标(0到n-1),所以永远使用n进行取模;
③. 数数的时候,只有元素不为标志值的时候才计入count统计。 -
自定义单向循环链:solveBySelfLinkedList
①. 通过单向链表构成一个环形结构
②. 通过遍历把初始指针tmp指移动到第K个元素,注意的是:K可能大于N,不过没关系,毕竟,是循环链表,到最后一个的时候,会回到头部;
③. 通过一个计数器cout,来记录数了几下,当count等于M-1了,就表明下一个节点要被删除了,此时tmp指向被删除节点的前一个节点,接下来,进行删除即可
④. 当链表为空了结束程序(empty方法判断)
这种方法需要查看自定义单向循环链表的实现:
https://blog.csdn.net/mail_liuxing/article/details/97976000 -
总结比较:1方法使用系统的API,对API很熟,3方法效率较高,比较推荐。
四、代码实现
package com.firewolf.javabase.s003_linkedlist.realcase;
import com.firewolf.javabase.s003_linkedlist.CyclicSingleLinkedList;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* 约瑟夫环 问题描述 N个人围成一圈,从第K个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,K=1,M=5, 被杀掉的顺序是:5,4,6,2,3,1。也就是说,最终1会活下来
*/
public class Josephus {
public static void main(String[] args) {
// Scanner scanner = new Scanner(System.in);
//
// System.out.println("请输入总人数:");
// int N = scanner.nextInt();
//
// System.out.println("请输入开始位置:");
// int K = scanner.nextInt();
//
// System.out.println("数到第几个展出死?");
// int M = scanner.nextInt();
int N = 10, K = 1, M = 5;
System.out.printf("%d个人参与游戏,从第%d个人开始数,每数%d个出圈!\n", N, K, M);
solveBySelfLinkedList(N, K, M);
solveByJdkLinkedList(N, K, M);
solveByArray(N, K, M);
solveByArrayefficient(N, K, M);
}
/**
* 使用jdk的链表来实现
*
* @param n 人数
* @param k 开始位置
* @param m 数几个
*/
private static void solveByJdkLinkedList(int n, int k, int m) {
LinkedList<Integer> list = new LinkedList<>();
int i = 1;
while (i <= n) {
list.add(i++);
}
List<Integer> result = new ArrayList<>();
//要判断的位置,其实就是从开始位置后面再加上m-1个元素(这样就是数到m个),第k个的下标为k-1,所以第一次要统计的位置是 (k-1)+(m-1) = k+m -2
int tartgetIndex =
(k + m - 2) % list.size(); //第一个被点到的下标位置,由于下标从0开始,需要减去2,假设n,k,m分别为6,1,5,那么此时targetIndex = 4(也就是第5个元素)
Integer remove = list.remove(tartgetIndex);
result.add(remove);
while (!list.isEmpty()) {
tartgetIndex = (tartgetIndex + m - 1) % list
.size(); //这个时候,targetIndex往后加上m-1个就是m个了,因为删除了原来的targetIndex位置的元素,后面的会补上来,所以这里也有一个
remove = list.remove(tartgetIndex);
result.add(remove);
}
System.out.println("Jdk数组结果顺序为:" + result);
}
/**
* 使用数组来解决,不需要不停的删除数组中元素,提高效率
*
* @param n 人数
* @param k 开始位置
* @param m 数几个
*/
private static void solveByArrayefficient(int n, int k, int m) {
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = i + 1;
}
List<Integer> result = new ArrayList<>();
int target = k - 1;
while (result.size() != n) {
int count = 0;
while (count < m) {
target = (target) % n;
if (array[target] != 0) { //表示没有出去的
count++; //如果这个位置没有出去,就计数
}
target++; //查看下一个元素是否满足
}
int tmp = target - 1; //因为在while里面target到了下一个位置,所以为了拿到需要被删除的位置,需要-1
result.add(array[tmp]);
array[tmp] = 0; //已经出去了的,清空为0
}
System.out.println("高效数组结果顺序为:" + result);
}
/**
* 使用数组来解决,需要不停的删除数据,也就是需要不停的移动元素 思路和使用jdk中的LinkedList类似
*
* @param n 人数
* @param k 开始位置
* @param m 数几个
*/
private static void solveByArray(int n, int k, int m) {
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = i + 1;
}
List<Integer> result = new ArrayList<>();
int count = 0;
int tartgetIndex = (k + m - 2) % (array.length - count);
Integer remove = deleteArrayByIndex(tartgetIndex, array);
result.add(remove);
count++;
while (count < n) {
tartgetIndex = (tartgetIndex + m - 1) % (array.length - count);
remove = deleteArrayByIndex(tartgetIndex, array);
result.add(remove);
count++;
}
System.out.println("数组计算结果顺序为:" + result);
}
private static int deleteArrayByIndex(int index, int[] array) {
int value = array[index];
for (int i = index + 1; i < array.length; i++) {
array[i - 1] = array[i];
}
return value;
}
/**
* 使用自定义的链表来实现
*
* @param n 人数
* @param k 开始位置
* @param m 数几个
*/
private static void solveBySelfLinkedList(int n, int k, int m) {
CyclicSingleLinkedList<Integer> personNos = new CyclicSingleLinkedList<>();
int i = 1;
while (i <= n) {
personNos.addLast(i++);
}
List<Integer> list = personNos.josephusRemove(k, m);
System.out.println("自定义链表计算结果顺序为:" + list);
}
}
其中com.firewolf.javabase.s003_linkedlist.CyclicSingleLinkedList#josephusRemove如下:
/**
* @param from 开始位置
* @param count 数几下
* @return 返回被删除元素的顺序
*/
public List<T> josephusRemove(int from, int count) {
if (empty()) {
return new ArrayList<>();
}
List<T> deletes = new ArrayList<>();
//找到开始数数的位置
int from1 = 1;
CyclicNode tmp = first;
while (from1++ < from) {
tmp = tmp.next;
}
while (!empty()) {
//找到要被删除的前一个节点
int count1 = 1;
while (count1 < count - 1) {
count1++;
tmp = tmp.next;
}
//执行删除
T data = tmp.next.data;
if (tmp.next == first) {
removeFirst();
} else if (tmp.next == last) {
removeLast();
} else {
tmp.next = tmp.next.next;
size--;
}
if (!empty()) { //后移下不标,从被删除的下一个元素开始数
tmp = tmp.next;
}
deletes.add(data);
}
return deletes;
}