JavaScript三种方法解决约瑟夫环问题的方法

目录
  • 概述
  • 问题描述
  • 循环链表
  • 有序数组
  • 数学递归
  • 总结

概述

约瑟夫环问题又称约瑟夫杀人问题或丢手绢问题,是一道经典的算法问题。问题描述也有很多变式,但大体的解题思路是相同的。本篇将以循环链表、有序数组、数学递归三种方式来解决约瑟夫环问题。

问题描述

先来看一下什么是约瑟夫环问题?

在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。

到了今天约瑟夫环问题的描述一般变成了:

在一间房中有N个人,所有人围成一圈顺时针开始报数,每次报到M的人退出游戏。退出游戏的下一个人再次重新开始报数,报到M的人再退出,依此循环,直到游戏只剩下最后一个人,则最后一个人的编号是多少?

循环链表

循环链表的解题思路比较简单,只需要将问题描述转换成代码即可。房间中的N个人组成一个长度为N的链表,首尾相接组成循环链表。列表中的每一项的值即为每个人的编号,在数到M时将该项(记为x)剔除,即该项的前一项的next不再指向x,而是x.next。依此规律遍历链表,直至链表中只剩下一项。

下面看一下代码实现。

function createList(num) {
    //链表节点的数据结构
    function createNode(value) {
        return {
            value: value,
            next: ''
        }
    }
    //链表头节点
    let head = createNode(1);
    let node = head;
    //自头节点之后创建节点之间的关联关系
    for (let i = 2; i <= num; i++) {
        node.next = createNode(i);
        node = node.next;
    }
    //最后一个节点指向头节点,构成循环链表
    node.next = head;
    return head;
}
function deleteListNode(num, nth) {
    //创建数据长度为num的循环链表
    let node = createList(num);
    //链表长度>1时,继续下一轮
    while (num > 1) {
        for (let i = 1; i <= nth - 1; i++) {
            if (i == nth - 1) {
                //i为nth-1,则node.next即为第nth个节点。剔除node.next
                node.next = node.next.next;
                //链表长度--
                num--;
            }
            node = node.next;
        }
    }
    //剩余的最后一个节点的value值即为最后一人编号
    return node.value
}
//deleteListNode(m,n)即可得到最终结果

有序数组

用有序数组来模拟循环链表,将数组第一次遍历剔除完成后剩余的数组项组成一个新的数组,再对新数组进行新一轮的遍历剔除,依此循环,直到数组长度为1。

下面看一下代码实现。

function jhonRing(num, nth) {
    let arr = [];
    //创建有序数组
    for (let i = 1; i <= num; i++) {
        arr[i - 1] = i;
    }
    //当数组长度大于nth时,数组不用循环遍历也可找到需要剔除的数据
    while (arr.length >= nth) {
        let newArr = [];
        //将数组末尾剩下的数据转移到新数组的前方,新一轮遍历从生于的数据开始
        let times = arr.length - arr.length % nth;
        newArr = arr.slice(times)
        arr.slice(0, times).map((item, index) => {
            //将除了剔除数据之外的其他数据加入新的数组
            if ((index + 1) % nth !== 0) {
                newArr.push(item)
            }
        })
        arr = newArr;
    }
    //当数组长度小于nth时,数组需要循环遍历才能找到要剔除的数据,通过取余操作可减少遍历的次数
    while (arr.length > 1) {
        //取余获取要剔除的数据的下标
        let index = nth % arr.length - 1;
        //剔除数据的后半部分与前半部分组成新的数组
        let newArr = arr.slice(index + 1).concat(arr.slice(0, index));
        arr = newArr;
    }
}
//jhonRing(num, nth)即可得到最终结果

用有序数组模拟链表的操作看上去跟链表差不多,时间复杂度看上去也一样,甚至代码也比不上链表简洁,但是为什么仍然要把这个方式提出来呢?这种方法的优势体现在M>>N的情况下,有序链表通过取余的方式有效的减少了循环遍历数组的次数。以N为3,M为100为例,链表需要循环遍历100/3+1次,而有序数组则根据取余操作的结果100/3-1=0,直接得到了要剔除的数据下标为0。

数学递归

用数学问题求解约瑟夫环问题时极易找不到一条有效的规律总结路线,下面以M=3举例讲述一下总结规律时走过的弯路。(可跳过无效的思路,直接阅读下方的有效思路)

比较多次数据量较小时的结果(❌)

N=1时,f(1,3)=1;
N=2时,f(2,3)=2;
N=3时,f(3,3)=2;
N=4时,f(4,3)=1;
N=5时,f(5,3)=4;
N=6时,f(6,3)=1;
N=7时,f(7,3)=4;
N=8时,f(8,3)=7;
N=9时,f(9,3)=1;
N=10时,f(10,3)=4;

通过举例发现,最终结果并不总是在某几个数之间,除了1,2,4以外还出现7,则之后的结果也有可能有类似的情况,即最终结果并不总是局限于某一个数之间。f(3,3) f(6,3) f(9,3)等N为M的倍数的情况并没有得到相同的结果,可见最终结果与3的倍数之间并不存在某种特殊联系。因此通过这种积累比较数据量较小时的结果来寻找规律的方案不合适。

比较前几次剔除数据之间的关系(❌)

//将N个人自1-N进行编号
1, 2, 3, 4........N-2,N-1, N
//第一次剔除的编号为M或M%N,可总结为M%N,记为K。则第二次剔除时的序列变为
K+1,K+2,.......N,1,2......K-1
//则第二次剔除的编号为K+M%(N-1) || M%(N-1)-(N-K-1)
依此得到规律
当N-(K+1)>M%(N-1)时,f(n)=f(n-1)+M%(N-(n-1))
当N-(K+1)<M%(N-1)时,f(n)=M%(N-(n-1))-(N-f(n-1)-1)

依此规律计算会发现,没有进行第二圈遍历时得到的结果是正确的,遍历进入第二圈之后的结果错误。根本原因在于进入第二圈之后的数据不再连续,第一圈遍历时剔除出的数据会影响第二圈遍历的结果,故此方案不合适。

自上而下分析问题,自下而上解决问题(✅)

用递归的思路去分析约瑟夫环问题。以N=10,M=3为例分析。

//将10个人从0开始编号(稍后解释为什么不能从1开始编号)
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
//将最后一个出列的人的编号记做f(10,3)
//在10个人编号后,第一个人出列后,得到新的队列及编号
3, 4, 5, 6, 7, 8, 9, 0, 1
//为了使新队列的编号连续,我们可以将新队列记做A,写作
3, 4, 5, 6, 7, 8, 9, 10%10, 11%10
//若将一个9人普通队列记做B,写作0,1,2,3,4,5,6,7,8的最终结果记做f(9,3),则新队列A的结果则可以表示为(f(9,3)+3)%10
//9人队列A是10人队列剔除一人后得到的,则9人队列按N=9,M=3,初始编号为3的规则进行游戏后得到的结果必然与N=10,M=3,初始编号为0的队列最后出列的人相同。
//故10人队列最后出列的人编号与9人队列A出列的人编号之间存在关系f(10,3)=(f(9,3)+3)%10

基于以上可以得到结论f(N,M)=(f(N-1,M)+M)%N。则最终编号的结果即为f(N,M)+1。
为什么编号不能从1开始呢?因为取余操作的返回结果是从0开始。

function Josephus(num,nth){
    if(num==1){
        return 0;
    }else{
        return (Josephus(num-1,nth)+nth)%num
    }
}
//Josephus(N,M)+1即为最终编号

对递归函数做一下优化

function JosephusR(num,nth){
    let value = 0;
    for(let i=1;i<=num;i++){
        //此处为对i取余,上述递归中num也是在逐渐变小的,所以此处为i而非num
        value=(value+nth)%i;
    }
    return value+1;
}
//JosephusR(N,M)即为最终编号

总结

通过数学递归方式的优化,将约瑟夫环问题的时间复杂度从最初的O(mn)降低到O(n)。通过循环链表的方式来解决问题确实是最快最容易想到的思路,但是这种数学递归的方式对解决此类算法问题来说更有思考的价值。

到此这篇关于JavaScript三种方法解决约瑟夫环问题的方法的文章就介绍到这了,更多相关JavaScript 约瑟夫环内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • javascript循环链表之约瑟夫环的实现方法

    前言 传说在公元1 世纪的犹太战争中,犹太历史学家弗拉维奥·约瑟夫斯和他的40 个同胞被罗马士兵包围.犹太士兵决定宁可自杀也不做俘虏,于是商量出了一个自杀方案.他们围成一个圈,从一个人开始,数到第三个人时将第三个人杀死,然后再数,直到杀光所有人.约瑟夫和另外一个人决定不参加这个疯狂的游戏,他们快速地计算出了两个位置,站在那里得以幸存.写一段程序将n 个人围成一圈,并且第m个人会被杀掉,计算一圈人中哪两个人最后会存活.使用循环链表解决该问题. 看到这个问题首先想到的是要用到循环链表,还有就是要计算

  • 一个报数游戏js版(约瑟夫环问题)

    这个也算是老题目了,园子里边也曾针对此题有过激烈的讨论,那时候追求用oo来解决.如今既然又有人提了出来,我便抽了点时间写了写自己的想法: 复制代码 代码如下: <script type="text/javascript"> var a_game = function(pNum){ var players = []; for(var i=1;i<=pNum;i++){ players.push(i); } var flag=0; while(players.length

  • JavaScript三种方法解决约瑟夫环问题的方法

    目录 概述 问题描述 循环链表 有序数组 数学递归 总结 概述 约瑟夫环问题又称约瑟夫杀人问题或丢手绢问题,是一道经典的算法问题.问题描述也有很多变式,但大体的解题思路是相同的.本篇将以循环链表.有序数组.数学递归三种方式来解决约瑟夫环问题. 问题描述 先来看一下什么是约瑟夫环问题? 在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀

  • C语言基于循环链表解决约瑟夫环问题的方法示例

    本文实例讲述了C语言基于循环链表解决约瑟夫环问题的方法.分享给大家供大家参考,具体如下: 概述: 约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(以编号1,2,3,-,n分别表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列:他的下一个人又从 1 还是顺时针开始报数,数到 m 的那个人又出列:依次重复下去,要求找到最后出列的那个人. 例如有 5 个人,要求从编号为 3 的人开始,数到 2 的那个人出列: 出列顺序依次为: 编号为 3 的人开始数 1

  • python超简单解决约瑟夫环问题

    本文实例讲述了python超简单解决约瑟夫环问题的方法.分享给大家供大家参考.具体分析如下: 约瑟环问题大家都熟悉.题目是这样的.一共有三十个人,从1-30依次编号.每次隔9个人就踢出去一个人.求踢出的前十五个人的号码: 明显的约瑟夫环问题,python实现代码如下: a = [ x for x in range(1,31) ] #生成编号 del_number = 8 #该删除的编号 for i in range(15): print a[del_number] del a[del_numbe

  • JavaScript三种获取URL参数值的方法

    目录 前言 URLSearchParams URL 纯JS 前言 在 URL 中,查询参数字符串值通常提供有关请求的信息,例如搜索参数或正在使用的对象的 ID.如果在前端处理任何业务或请求逻辑,了解如何从 URL 中检索查询字符串值非常重要.本文分享三种从 URL 获取参数的方法. URLSearchParams 除 IE 11 之外的所有主要浏览器版本都支持该 URLSearchParams 接口.它通过解析 URL 的查询字符串并提供访问值的方法来工作. 例如: 此接口的缺点之一是您必须仅将

  • php基于环形链表解决约瑟夫环问题示例

    本文实例讲述了php基于环形链表解决约瑟夫环问题.分享给大家供大家参考,具体如下: 先来重温一下约瑟夫环问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉.例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1. 前面介绍了关联数组解决约瑟夫环的方法,环形链表解决约瑟夫环的方法如下: <?php header("content-type:text/html;charset=utf-8"); class Child{ public $no;

  • JavaScript三种绑定事件方式及相互之间的区别分析

    本文实例讲述了JavaScript三种绑定事件方式及相互之间的区别.分享给大家供大家参考,具体如下: JavaScript三种绑定事件的方式: 1. <div id="btn" onclick="clickone()"></div> //直接在DOM里绑定事件 <script> function clickone(){ alert("hello"); } </script> 2. <div i

  • PHP实现约瑟夫环问题的方法分析

    本文实例讲述了PHP实现约瑟夫环问题的方法.分享给大家供大家参考,具体如下: 一.概述 先来看看网上比较常见的约瑟夫环问题描述:约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围.从编号为k的人开始报数,数到m的那个人出列:他的下一个人又从1开始报数,数到m的那个人又出列:依此规律重复下去,直到圆桌周围的人全部出列.通常解决这类问题时我们把编号从0~n-1,最后结果+1即为原问题的解. 二.实现代码 1. 循环 function circ

  • Javascript三种字符串连接方式及性能比较

    第一种:用连接符"+"连接字符串 str="a"; str+="b"; 这种方法相对以下两种,最便捷快速.建议100字符以下的连接使用这种连接方式. 第二种:以数组作为中介,使用jion函数进行连接 var arr=new Array(); arr.push(a); arr.push(b); var str=arr.join(""); 第三种:利用对象属性连接字符串 function stringConnect(){ this

  • php解决约瑟夫环算法实例分析

    本文实例讲述了php解决约瑟夫环算法.分享给大家供大家参考,具体如下: 今天偶遇一道算法题 "约瑟夫环"是一个数学的应用问题:一群猴子排成一圈,按1,2,-,n依次编号.然后从第1只开始数,数到第m只,把它踢出圈,从它后面再开始数, 再数到第m只,在把它踢出去-,如此不停的进行下去, 直到最后只剩下一只猴子为止,那只猴子就叫做大王.要求编程模拟此过程,输入m.n, 输出最后那个大王的编号. 方法一:递归算法 function killMonkey($monkeys , $m , $cu

随机推荐