前言:
约瑟夫问题是基于环形链表这种数据结构基础上的算法,曾经在腾讯等大厂中出现过,
对于初学者而言是一个不可或缺的基础技能,本篇博客主要从以下几点展开阐述
什么是约瑟夫问题 --> 环形队列创建思路 --> 环形队列的实现代码与代码分析 --> 约瑟夫问题的思路 --> 约瑟夫问题的算法解决
如果觉得博客写的好的话,不妨戳戳介个 作者的数据结构博客集了解更多的内容
往期精彩:
如果还能关注一下,那就太棒了!!!
约瑟夫问题的概述:
一群小孩围坐在一起,他们的编号分别从1,2,3,4....到n,然后他们就开始了一个特别的游戏,我称之为"幸运者游戏",游戏的玩法是这样的:裁判约定从第K个人开始报数(这个编号为k的人报1),然后按顺序报下去,假设有一个人报到m,那么这个人就会出局,然后下一个人接着报数,又假设有一个人报到了m,这个人就出局了,以此不断循环,就会剩下最后一个人,那么这个人就会被称为"幸运者"!
约瑟夫问题解决方法的探究:
首先,我们会下意识的想到用普通一位数组或者是环形队列去解决这个问题,实际上,一维数组和环形队列确实可以解决,但是太过于抽象,不够形象,而且在代码实现上更加的复杂,不利于我们去实现代码,对此,我们引入了一个新的数据结构环形单向链表!
环形单向链表
概念
单向链表不再有头节点和尾节点之分,所有的节点都联系再一起,形成一个类似于圆环的东西,这就是我们的单向链表
创建思路
- 创建一个节点类,该节点应该有data域(存放该节点的数据),next域(下一个节点的内存地址)
- 创建一个环形链表类,用于管理我们的节点,应该有节点first属性
- 创建一个方法,用于添加节点
- 弄完了!!!
代码实现:
- 创建一个节点类
public class Node {
int no;
Node next;
public Node() {
}
public Node(int no) {
this.no = no;
}
}
- 创建一个单向链表类(省略),并加上添加节点的方法
public void add(int num){
if(num < 1) {
System.out.println("num[单向环形链表的长度]不合理!!!");
return;
}
Node cur = null;
for (int i = 1; i <= num; i++) {
var node = new Node(i);
// i=1的时候说明是创建第一个节点的时候,应该用first指向该节点
if(i == 1) {
first = node;
//每一个新创建节点的next都应该指向first,这样才能形成一个环
node.next = first;
} else {
//因为cur指向最新的节点,next就可以增加节点
cur.next = node;
node.next = first;
}
//将cur定位到最新的节点,上面两种情况都适应
cur = node;
}
}
- 添加方法第一步是校验数据的可靠性,如果num小于或等于0,说明人数小于或等于0,明显错误,应该通过if去校验
- 创建一个cur引用,用于指向最新的节点
- 通过for循环去创建节点
约瑟夫问题的解决
创建思路:
- 首先要校验数据的合理性
- 创建一个辅助引用,用于后续约瑟夫问题的解决(该引用helper需放到最后)
- 将两个引用(first和helper)移动到指定位置
- 将两个引用根据m移动,最后将出局人输出并删除
// size表示总人数,用于校验数据,主要是用于对k的校验
public void josephSolve(int k,int m,int size) {
if(first == null || k < 1 || k > size) {
System.out.println("参数错误,方法无法执行");
}
var helper = first;
// 目的是让helper去到最后一个位置
while(helper.next != first) {
helper = helper.next;
}
// 我要要让first去到第k个人,第k个人开始报数,他应当成为first,helper紧随其后(实际上移动了k-1)
// 比如第三个人开始数 第一个移动到第三个只需要移动两次
for (int i = 0; i < k - 1; i++) {
helper = helper.next;
first = first.next;
}
while (true) {
// 判断是否只有一个人
if(helper == first) {
break;
}
// 数到m,实际上移动了m-1下
for (int i = 0; i < m - 1; i++) {
helper = helper.next;
first = first.next;
}
System.out.println("第" + first.no + "个人出局了");
first = first.next;
helper.next = first;
}
System.out.println("幸存者是" + first.no);
}
循环中倒数两行的图解:
-
Joseph就这样解决啦!
完整代码
package datastructure.chapter01.linked_list.circle_linked_list;
public class Node {
public int no;
public Node next;
public Node() {
}
public Node(int no) {
this.no = no;
}
}
package datastructure.chapter01.linked_list.circle_linked_list;
public class SingleCircleLinkedList {
private Node first;
/**
* 用于创建单向环形链表的长度
* @param num 单向环形链表的长度
*/
public void add(int num){
if(num < 1) {
System.out.println("num[单向环形链表的长度]不合理!!!");
return;
}
Node cur = null;
for (int i = 1; i <= num; i++) {
var node = new Node(i);
if(i == 1) {
first = node;
node.next = first;
} else {
cur.next = node;
node.next = first;
}
// 值得深究的代码
cur = node;
}
}
/**
* 用于输出单向环形链表
*/
public void print() {
if(first == null) {
System.out.println("单向环形链表的长度为0");
return;
}
var cur = first;
do {
System.out.println(cur.no);
cur = cur.next;
}while(cur != first);
}
/**
* 约瑟夫问题的解决方法
* @param k 从第K个人开始报数
* @param m 报数为m的人出局
* @param size 总人数,用于校验k,m合理性
*/
public void josephSolve(int k,int m,int size) {
if(first == null || k < 1 || k > size) {
System.out.println("参数错误,方法无法执行");
}
var helper = first;
while(helper.next != first) {
helper = helper.next;
}
for (int i = 0; i < k - 1; i++) {
helper = helper.next;
first = first.next;
}
while (true) {
if(helper == first) {
break;
}
for (int i = 0; i < m - 1; i++) {
helper = helper.next;
first = first.next;
}
System.out.println("第" + first.no + "个人出局了");
first = first.next;
helper.next = first;
}
System.out.println("幸存者是" + first.no);
}
}
结论:
约瑟夫问题的解决说难不难,说简单但是有一两行代码又具有其深度所在,希望看到最后的各位能真正理解了约瑟夫问题的解决,我总结几点重要的需要掌握的知识点
1.如何创建环形单向链表并添加节点
2.如何解决约瑟夫问题
投票环节: