js 打印数组_约瑟夫环的四种解决方案JS

本文详细介绍了使用数组、循环队列、循环链表和递归算法解决约瑟夫环问题的四种方法,每种方法都有详细的代码实现和逻辑解释。通过对不同数据结构和算法的应用,展示了如何在不同场景下有效地解决问题。同时,通过对比执行时间,探讨了各种解决方案的效率和适用性。
摘要由CSDN通过智能技术生成

31fd9dabd30c1100d1a1c4ff9089d3cb.png

传说罗马人占领了乔塔帕特,41 个犹太人被围堵在一个山洞里。他们拒绝被俘虏,而决定集体自杀,大家决定了一个自杀方案,41 个人围成一个圈,由第 1 个人开始顺时针报数,每报数为 3 的人立刻自杀,然后再由下一个人重新从 1 开始报数,依旧是每报数为 3 的人立刻自杀,依次循环下去。其中两位犹太人并不想自杀,是数学家约瑟夫和他的朋友,他们发现了自杀方案的规律,选了两个特定的位置,最后只剩下他们两人,而活了下来。那么这两个位置分别是什么?

这个问题转化成要解决的通用问题:即 n 个人围成一个圈,这 n 个人的编号从 0——(n-1), 第一个人(编号为0的人)从 1 开始报数,报数为 m 的人离开,再从下一个开始从 1 开始报数,报数为 m 的人离开,依次循环下去,直到剩下最后一个人(也可以剩最后两个,少循环一次就是了),那么,把最后一个人的编号打印出来。

第一种方案:我们先用数组的方式来解决这个问题,因为这是最容易理解的。

用一个数组来存储 n 个人在圈内的状态,全部标识为 1,即长度为 n 的数组所有元素都为 1 ,用一个报数器来记录报数了几次,只有被标识为 1 的人才能够报数,当报数器的值与 m 相等时,就让这个人离开,则标识为 0,并且让记录出圈人数的变量加 1,然后将报数器清零,当纪录变量等于 n-1 时,游戏结束。

下面是代码和注释:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style></style>
<script src=" https:// unpkg.com/@babel/standa lone/babel.min.js "></script>
<script type="text/babel"> //通过 babel 转译器,可以将 es6 语法转译成 es5 语法
function josephRing(n, m) {
//当参数不满足条件时,这个游戏无法进行
if(n<=1 || m<1) {
console.log("you can't play Joseph's game. n must be bigger than 1, m must be bigger than 0");
return;
}
let arr = new Array(n); //长度为n的数组,位置从0——n-1,就代表了 n 个人的编号
let count = 0; //纪录出圈人数
let num = 0; //报数器
for(let i=0;i<arr.length;i++) { //将数组所有元素设定为 1
arr[i] = 1;
}
//设定循环结束条件:当 count = n-1 时,游戏结束
while(count < n-1) {
for(let i=0;i<arr.length;i++) { //第二层循环,循环数组
if(arr[i] === 1) { //当这个位置的元素为 1 时,就执行接下来的代码
num++; //每经过一个元素为 1 的位置时,就让报数器加 1
if(num === m) { //当报数器等于 m 时,就执行接下来的代码
arr[i] = 0; //让这个位置的元素为 0,表示这个位置已经出圈了
count++; //纪录出圈人数的变量加 1
num = 0; //将报数器清零
}
//当 m = 1 时,只有当 count = n 才会退出第二层循环(for循环),此时数组内的所有元素都变为了 0,为了避免这个问题,必须要有这个 if 判断句,达到特定条件时强制退出
//其实当 m = 1时,结果就是 n,也可以将 m = 1 作为特殊情况来处理,即写在 while 循环以外,如此 m = 1 时就不会进入循环
if(count === n-1) {
break
}
}
}
}
//循环数组,找到元素为 1 的位置,将这个位置输出
for(let i=0;i<arr.length;i++) {
if(arr[i] === 1) {
console.log(i + 1 + ' is the winner');
}
}
}
//测试上面的代码,并且打印执行的时间,如此可以与其他解决方案的执行时间相比较
let start = new Date().getTime();
josephRing(10000, 3);
let end = new Date().getTime();
console.log('====' + (end - start) + '====');
</script>
</head>
<body>
</body>
</html>

第二种方案:用循环队列这种数据结构来解决这个问题,如果你不了解队列,可以跳过这里,不过也可以了解下什么是队列。

队列数据结构是遵循先进先出(也可以说成先来先服务)原则的一组有序的项。队列的尾部添加新元素,并从顶部(头部)移除元素。最新添加的元素必须排在队列的末尾。

首先将队列的构造函数实现出来,只写了要用到的方法,将这个文件存储为 queue.js

function Queue() {
//items 是一个私有属性,只能被 Queue 函数访问,不能被外部访问,所以方法必须写在构造函数内,才能访问到 items 这个属性,我们希望实例化对象只能访问在 Queue 函数内定义好的方法,实例化对象是不能访问这个属性的,比如实例化了一个 queue 对象,queue.items 返回的结果是 undefined,如果items能被外部访问到(比如你写成this.items = []),queue.items 就能访问到 items 这个属性,因为我们是用数组来存储队列的值的(当然,不一定非要用数组来存储值),就可以通过数组的方式随意更改这个队列的值,那么这个队列就不能叫队列了,你也可以从中间删除或添加值,破坏了队列先进先出的原则。但是用构造函数实现的队列,如果实例化对象很多的话,那么创造的属性和方法的副本就太多了,一个可行的方法是使用 es6 的 WeakMap 来实现队列,但扩展类无法继承私有属性,所以这个看实际需求来决定用哪种方式实现队列了
let items = [];
this.enqueue = function(element) { //向队列尾部添加一个(或多个)新的项
items.push(element);
};
this.dequeue = function() { //移除队列的第一项,并返回被移除的值
return items.shift();
};
this.size = function() { //返回队列包含的元素个数,与数组的length属性类似
return items.length;
};
}

有了队列这种数据结构,我们就可以拿来解决问题了:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="us-ascii">
<title></title>
<style></style>
<script src=" https:// unpkg.com/@babel/standa lone/babel.min.js "></script>
<script src="queue.js"></script> //引入 js 文件
<script type="text/babel">
function josephRing(n, m) {
if(n<=1 || m<1) {
console.log("you can't play Joseph's game. n must be bigger than 1, m must be bigger than 0");
return;
}
let queue = new Queue(); //实例化一个对象
for(let i=0;i<n;i++) { //将 0——n-1 的数字存入队列中
queue.enqueue(i);
}
while(queue.size() > 1) { //当队列中的项只剩一个时,循环结束
//从第 0 项开始,每循环一次,就从队列开头移除一项,再将其添加到队列末尾,所以只需要移动 m-1 次就报数了 m 次了
for(let i=0;i<m-1;i++) {
queue.enqueue(queue.dequeue());
}
queue.dequeue(); //当报数为 m 时,移除该项
}
//打印最后一项,并且加 1,为了对应 1——n
console.log(queue.dequeue() + 1 + ' is the winner');
}
let start = new Date().getTime();
josephRing(10000,3);
let end = new Date().getTime();
console.log('====' + (end - start) + '====');
</script>
</head>
<body>
</body>
</html>

第三种方案:用循环链表这种数据结构来解决问题。

链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称为指针或链接)组成。双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向下一个元素。循环链表可以像链接一样只是单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针,不是引用 null,而是指向第一个元素。

首先将循环双链表的构造函数实现出来,只写了要用到的方法,将文件存为 circularLinkedList.js

function CircularLinkedList() {
let Node = function(element) {
this.element = element;
this.next = null;
this.prev = null;
};
let head = null;
let tail = null;
let length = 0;
//从链表尾部添加一个新的项
this.append = function(element) {
let node = new Node(element);
let current;
if(!head) {
head = node;
tail = node;
}else {
current = tail;
current.next = node;
node.prev = current;
node.next = head;
tail = node;
head.prev = tail;
}
length++;
};
//返回元素在链表中的索引,如果链表中没有该元素则返回 -1
this.indexOf = function(element) {
let current = head,
index = 0;
do{
if(element == current.element) {
return index;
}
index++;
current = current.next;
}while(current.prev !== tail);
return -1;
};
//从链表的特定位置移除一项
this.removeAt = function(position) {
if(position > -1 && position < length) {
let current = head;
let previous;
let index = 0;
if(position === 0) {
head = current.next;
if(length === 1) {
tail = null;
}else {
head.prev = tail;
tail.next = head;
}
}else if(position === length-1) {
current = tail;
tail = current.prev;
tail.next = head;
head.prev = tail;
}else {
if(0 < position < length/2) {
while(index++ < position) {
previous = current;
current = current.next;
}
}else {
let current = tail;
let index = length-1;
while(position < index--) {
previous = current;
current = current.prev
}
}
previous.next = current.next;
current.next.prev = previous;
}
length--;
return current.element;
}else {
return null;
}
};
//根据元素,从链表中移除一项
this.remove = function(element) {
let index = this.indexOf(element);
return this.removeAt(index);
};
//由于链表项使用了 Node类,就需要重写继承自 JS 对象默认的 tostring 方法,让其只输出元素的值
this.toString = function() {
let current = head;
let string = '';
do{
string += current.element + ' ';
current = current.next;
}while(current.prev !== tail);
return string;
};
//返回链表包含的元素个数。与数组的 length 属性类似
this.size = function() {
return length;
};
//tail 变量是该构造函数的私有变量(这意味着它不能被构造函数外部访问和更改)。但是,如果我们需要在外部循环访问链表,就需要提供一种获取链表的第一个或最后一个元素的方法
this.getTail = function() {
return tail;
};
}

有了循环链表这种数据结构,我们就可以拿来解决问题了:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="us-ascii">
<title></title>
<style></style>
<script src=" https:// unpkg.com/@babel/standa lone/babel.min.js "></script>
<script src="circularLinkedList.js"></script>
<script type="text/babel">
function josephRing(n, m) {
if(n<=1 || m<1) {
console.log("you can't play Joseph's game. n must be bigger than 1, m must be bigger than 0");
return;
}
let circular = new CircularLinkedList();
for(let i=0;i<n;i++) {
circular.append(i);
}
//给实例化对象添加属性,为当前项,将链表的最后一个元素引用赋值给它
circular.current = circular.getTail();
//给实例化对象添加方法,将 current 往前移动 m 次,移动第一次时指向的是第一个元素,因为 current 最开始指向的是最后一个元素
circular.advance = function(m) {
while(m>0) {
this.current = this.current.next;
m--;
}
};
//当链表只剩最后一项时,循环结束
while(circular.size() > 1) {
circular.advance(m);
//以 current 元素为参数删除该项
circular.remove(circular.current.element);
}
console.log(parseInt(circular.toString()) + 1 + ' is the winner.');
}
let start = new Date().getTime();
josephRing(10000,3);
let end = new Date().getTime();
console.log('====' + (end - start) + '====');
</script>
</head>
<body>
</body>
</html>

第四种方案:用递归算法来解决问题,如果你不了解递归,那么这个方案是最难理解的,不过这是解决该问题最简洁最快的方式。

递推:人类固有的递推思维方式,也就是从前到后,从小到大,由易到难,由局部到整体

递归:从上向下层层展开,再从下到上一步步回溯,和堆栈,先进后出,后进先出是对应的,当堆栈里的数字全部清空时,递归算法也就结束了。

递归过程的每一步用的都是同一个算法,计算机只需要自顶往下不断重复。

递归的本质有两条:其一是自顶而下,其二是自己不断重复。

这个问题之所以想到用递归方式,基本逻辑是这样的:n 个人出局一个时,总人数就变成了 n-1 个人,此时要处理的问题实际上就是 n-1 个人的问题,以此类推下去,要处理的就是 n = 2 的问题,因为当 n = 1 时,游戏就结束了。那么用一个方程来表示 n = 2 的解决过程:f(2,m) 很容易看到的规律是:m 是双数留下的就是 0,m 是单数留下的就是 1,用 m%2 来表示结果(此为递归算法的基本条件)。然后要找到 f(3,m) 和 f(2,m) 的对应关系,更精确点来说是 f(n,m) 和 f(n-1,m) 的对应关系。

步骤为:先找基准条件,再通过一个简单的例子来找 f(n,m) 和 f(n-1,m) 的对应关系。

首先举一些简单的例子,找下规律:

这是要处理的问题:n = 5,m = 2
0 1 2 3 4 从第 0 位开始报数,第 1 位报数为 2
0 2 3 4 离开第一个人
2 3 4 0 (*) 上面的数字可以写成这样,因为从第 2 位开始报数
0 1 2 3 (**) 这是 n = 4, m = 2,要处理的问题,接下来就处理这个问题
0 2 3 离开第二个人
比较(*)式和(**)式,你可以找到规律:((**)+2)%5 则转化为(*)式了
2 3 0 (*) 上面的数字可以写成这样,因为从第 2 位开始报数
0 1 2 (**) 这是 n = 3,m = 2,要处理的问题,接下来就处理这个问题
0 2 离开第三个人
比较(*)式和(**)式,你可以找到规律:((**)+2)%4 则转化为(*)式了
2 0 (*) 上面的数字可以写成这样,因为从第 2 位开始报数
0 1 (**) 这是 n = 2,m=2,要处理的问 题,我们已经知道答案了,即 2%2 = 0
比较(*)式和(**)式,你可以找到规律:((**)+2)%3 则转化为(*)式了
0 离开第四个人, 游戏结束,那么要往上层回溯了
上面的公式可以让(**)式等价于(*)式,即在(**)式处理问题,得到的结果,通过这个公式就能得出(*)式的结果 因此我们可以得出对应关系的公式:f(n,m) = (f(n-1,m)+m)%n 即当我们得到了 f(n-1,m) 的解答时,f(n,m) 的解答也就出来了
递归的思路是从上往下分解,再一层层往上回溯的。当 n = 2,m = 2 时,我们得到的解是 0,将这个 0 套入公式,往上回溯一层,所以当 n = 3,m = 2 时,我们得到的解是 (0+2)%3 = 2,将这个 2 套入公式,往上回溯一层,所以当 n = 4,m = 2 时,我们得到的解是 (2+2)%4 = 0,将这个 0 套入公式,往上回溯一层,所以当 n = 5,m = 2 时,我们得到的解是 (0+2)%5 = 2,因此我们解决了 f(5, 2) 的问题。

下面是代码实现:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="us-ascii">
<title></title>
<style></style>
<script src=" https:// unpkg.com/@babel/standa lone/babel.min.js "></script>
<script type="text/babel">
function josephRing(n, m) {
if(n<=1 || m<1) {
console.log("you can't play Joseph's game. n must be bigger than 1, m must be bigger than 0");
return;
}
let r = 0;
for(let i=2; i<=n; i++) {
//会先计算 n = 2 时的结果,最终得到的 r 就是胜利者
r = (r + m) % i;
}
console.log(r + 1 + ' is the winner.');
}
let start = new Date().getTime();
josephRing(10000,3);
let end = new Date().getTime();
console.log('====' + (end - start) + '====');
</script>
</head>
<body>
</body>
</html>

到此四种方案已经全部介绍完了,当然还有很多很多的方式来解决同一个问题,你可以执行一下这些代码,比较下这几种方案的优缺点。有任何疑惑和建议可以留言,我会及时回复。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值