最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    正文概述 掘金(掘金安东尼)   2021-07-15   702

    承接上一篇:理解 P/NP 问题时,我产生了一种已经触碰到人类认知天花板的错觉?!

    我们目前的世界仍是基于 P ≠ NP,所以有理由相信:只要我们把牌洗的足够乱,幸运女神或许就会降临。(生活就像英雄联盟,运气游戏而已~)

    卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    本篇带来的就是:如何把牌洗的足够乱的 洗牌算法

    从青铜到王者,面试和实战都用得到! 点赞收藏 ✨

    闲言少叙,直接奥力给!!

    青铜洗牌 ?

    题目:给你一副崭新的扑克牌(54 张),你如何 “洗乱” 它??

    咱青铜玩家通常很暴躁!

    不就是洗牌嘛!聪明的青铜玩家,先将问题抽象为算法模型!

    问题转化为:

    已知:一个数组 nums = [1,2,3,4,5,...,51,52,53,54],求解:一个乱序的新数组 radomNums。(简直不能再 nice 了)

    然后采用 【暴力抽取】

    在 1 至 54 之前随机生成一个整数,然后把它放到新数组里,然后再随机生成一个整数,如果和之前生成的没重复,直接放入新数组,如果和之前重复了,那再随机生成一个......

    这样直至数组中所有数字都被抽取放到新数组,最终得解!

    代码实现:

    // 生成 nums:
    let nums=[]
    for(let i=1;i<=54;i++){
        nums.push(i)
    }
    
    // 青铜洗牌算法:
    let count = 0 
    const shuffle = function(nums) {
        let radomNums = []
        while (radomNums.length < nums.length) {
            count++; // count 计数洗牌次数
            let rand = randOne()
            if(radomNums.includes(rand)){ // 随机数重复
                rand = randOne() // 再次生成
            }else{
                radomNums.push(rand)
            }
        }
        return radomNums
    }
    
    // 在 1 至 54 之间任意取一整数;
    const randOne= function() {
        return Math.floor(Math.random() * 54) + 1;
    }
    
    console.log(shuffle(nums))
    console.log(count)
    // (54) [22, 48, 13, 23, 15, 12, 18, 50, 5, 28, 27, 52, 46, 16, 40, 6, 33, 9, 41, 30, 54, 14, 36, 53, 17, 2, 11, 37, 42, 3, 8, 21, 25, 20, 34, 32, 35, 4, 43, 26, 38, 24, 10, 45, 31, 49, 44, 19, 51, 7, 1, 39, 47, 29]
    // 271
    

    完美~ 青铜玩家暴力洗牌,简单直白,在万军丛中直取敌将首级!!

    白银洗牌 ?

    白银玩家看了青铜玩家的操作,不禁放声大笑!

    “痴线~”(sb)

    把上述代码拷贝至控制台运行发现,基本上打乱这副扑克牌要洗 200 ~ 300 次!因为越往后,生成随机数重复的概率就越大!

    只有痴线才会洗这么多次吧~

    于是,白银玩家开始操作起来!遥感

    关键词:【交换位置】

    将牌随机分成两堆,让它们交换,然后再随机分成两堆,再让它们交换,然后再随机分出两堆......这样重复洗十几、二十次后,完成洗牌。

    实际上,在现实中,我们玩牌,大部分玩家也是这样去洗的,它也叫【印度洗牌法】(难道是阿三发明的?)

    卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    代码实现:

    // 生成 nums:
    let nums=[]
    for(let i=1;i<=54;i++){
        nums.push(i)
    }
    
    // 白银洗牌算法:
    const shuffle = function(nums){
        let radomNums = JSON.parse(JSON.stringify(nums))
        for(let i = 0;i < 20;i++){
            let randIndex1 = randOneIndex()
            let randIndex2 = randOneIndex()
    
            if(randIndex2 < randIndex1){ // 若 rand2<rand1,二者替换
                randIndex1 = randIndex1 + randIndex2
                randIndex2 = randIndex1 - randIndex2 
                randIndex1 = randIndex1 - randIndex2
            }
    
            let radomBlock = radomNums.slice(randIndex1,randIndex2 + 1)
            radomNums = radomNums.slice(0,randIndex1).concat(radomNums.slice(randIndex2,53)).concat(radomBlock)
        }
        return radomNums
    }
    
    // 在 0 至 53 之间任意取一整数作数组下标;
    const randOneIndex= function() {
        return Math.floor(Math.random() * 54);
    }
    
    console.log(shuffle(nums))
    // (54) [30, 9, 7, 28, 29, 39, 45, 46, 47, 48, 49, 50, 51, 52, 24, 25, 26, 27, 40, 42, 43, 44, 38, 31, 14, 8, 41, 22, 32, 19, 20, 1, 2, 10, 11, 12, 13, 16, 15, 53, 23, 3, 4, 5, 6, 21, 17, 18, 33, 34, 35, 36, 37, 42]
    

    白银,永远滴神!只用洗 20 次,就能把牌洗乱!

    黄金洗牌 ?

    这时,黄金玩家又笑了:“呵呵,你个渣渣白银,根本还没看懂题目!”

    “你懂不懂洗牌问题最最关键的 “洗乱” 二字意义所在?!”

    黄金洗牌来揭晓答案:

    洗 54 张牌,随机结果需覆盖所有情况就应该是 54 张牌的排列方式,A5454,即 54!(54 的阶层)种随机结果。

    两两对换:

    洗 1 次,会得到 n 种可能的结果;

    洗 2 次,会得到 n*(n-1) 种结果;

    洗 3 次,会得到 n*(n-1)*(n-2) 种结果;

    ......

    洗 n 次之后,我们才满足了:随机结果【覆盖所有情况】,并且所有随机结果【出现概率相等】

    所以,必须洗 54 次,才能达到目的。

    代码实现:

    // 生成 nums:
    let nums=[]
    for(let i=1;i<=54;i++){
        nums.push(i)
    }
    
    // 黄金洗牌算法:
    const shuffle = function(nums) {
    
        // 高手都用 slice(0) 复制数组
        const radomNums = nums.slice(0);
        let n = radomNums.length;
    
        // 产生的结果有 n! 种可能
        for (let i = 0; i < n; i++) {
    
            // 从 i 到 n-1 随机选一个
            const rand = randOne(i, n - 1); 
    
            // 交换nums数组i和rand下标的两个元素
            [ radomNums[i], radomNums[rand] ] = [ radomNums[rand], radomNums[i] ];
        }
    
        return radomNums;
    };
    
    // 获取闭区间 [n, m] 内的一个随机整数
    const randOne= function(n, m) {
        return Math.floor(Math.random() * (m - n + 1)) + n;
    };
    
    
    console.log(shuffle(nums))
    // (54) [39, 40, 11, 35, 1, 47, 33, 9, 44, 32, 31, 45, 41, 4, 51, 42, 8, 10, 16, 14, 18, 17, 13, 6, 34, 53, 48, 5, 15, 22, 38, 37, 49, 43, 3, 20, 26, 52, 30, 19, 7, 50, 12, 21, 46, 36, 23, 27, 28, 25, 2, 29, 24, 54]
    

    铂金洗牌 ?

    正在网吧的铂金大神看到上面这些弟弟,笑出猪叫~~~

    但凡上点网,学点攻略,就不至于在洗牌这个问题上没听说过 Fisher-Yates 洗牌算法!

    思路:

    随机生成 1 至 54 之间的整数,将它和数组的最后一位替换;

    然后再在 1 至 53 之间随机生成一位整数,将它和数组的倒数第二位替换;

    然后再 1 至 52 之间随机生成一位整数,将它和数组的倒数第三位替换;

    ......

    直至在 1 至 1 之间随机生成一位整数(即 1),将它和数组第 1 位替换(即替换自身);

    卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    这样做,时间复杂度为 O(n),且任意一张牌出现的概率都是 1/52,满足:随机结果覆盖所有情况,随机结果出现概率相等!

    数学证明:一张牌被放到第 i 个位置的机率为 P,则 P 会等于前 i-1 个位置都未选到这张牌且第 i 个位置选到这张牌。

    卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    代码实现:

    // 生成 nums:
    let nums=[]
    for(let i=1;i<=54;i++){
        nums.push(i)
    }
    
    // 铂金洗牌算法:
    const FYShuffle = function (nums) {
    
        const radomNums = nums.slice(0);
        let len = radomNums.length;
        
        while (len > 1) {
            let rand = Math.floor(Math.random() * len);
            len--;
            let temp = radomNums[len];
            radomNums[len] = radomNums[rand];
            radomNums[rand] = temp;
        }
    
        return radomNums;
    }
    
    console.log(FYShuffle(nums))
    // (54) [47, 17, 33, 13, 37, 26, 20, 39, 45, 44, 25, 40, 49, 7, 36, 38, 6, 15, 31, 18, 52, 46, 28, 11, 43, 1, 22, 19, 53, 9, 14, 27, 35, 8, 51, 42, 50, 2, 23, 5, 30, 54, 4, 21, 29, 16, 10, 24, 48, 34, 32, 12, 41, 3]
    

    钻石洗牌 ?

    钻石大神拍案站起,惊呼道:鸽尾式洗牌法(Riffle Shuffle),才是永远滴神!

    现实中很多扑克高玩都会这样洗吧(一图胜千言)

    卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    原理:将数组一分为二,再穿插合并,再不断重复这样的操作;

    卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    研究表明:用鸽尾式洗牌法【洗七次】是最有效的打乱手法!(谁研究的?自行 Google)

    代码实现:

    // 生成 nums:
    let nums=[]
    for(let i=1;i<=54;i++){
        nums.push(i)
    }
    
    // 钻石洗牌算法:
    const RShuffle = function (arr) {
    
        let radomNums = nums.slice(0);
        for(let i = 0;i < 7;i++){
            let randIndex = randOneIndex()
            let arr1 = radomNums.slice(0,randIndex)
            let arr2 = radomNums.slice(randIndex,55)
            radomNums = aryJoinAry(arr1 ,arr2)
        }
        return radomNums;
    }
    
    // 两个数组穿插合并
    const aryJoinAry = function (ary,ary2) {
        var itemAry=[];
        var minLength;
        //先拿到两个数组中长度较短的那个数组的长度
        if(ary.length>ary2.length){
            minLength=ary2.length;
        }
        else{
            minLength=ary.length;
        }
        //将两个数组中较长的数组记录下来
        var longAry=arguments[0].length>arguments[1].length?arguments[0]:arguments[1];
        //循环范围为较短的那个数组的长度
        for (var i = 0; i < minLength; i++) {
            //将数组放入临时数组中
            itemAry.push(ary[i]);
            itemAry.push(ary2[i])
        }
        //itemAry和多余的新数组拼接起来并返回。
        return itemAry.concat(longAry.slice(minLength));
    }
    
    
    // 在 0 至 53 之间任意取一整数作数组下标;
    const randOneIndex= function() {
        return Math.floor(Math.random() * 54);
    }
    
    console.log(RShuffle(nums))
    // (54) [1, 4, 19, 2, 38, 51, 6, 37, 15, 9, 45, 43, 7, 52, 16, 21, 39, 53, 24, 26, 44, 54, 40, 5, 32, 10, 46, 22, 33, 27, 3, 11, 18, 28, 47, 12, 17, 29, 8, 13, 41, 30, 48, 14, 34, 31, 20, 23, 49, 35, 25, 42, 50, 36]
    

    大师洗牌 ?

    大师看到这里,笑而不语。

    大师说:“把牌洗乱固然重要,但是能不能,把牌洗乱之后,还能发给自己想要的牌?!”

    —— 大师,我悟了!这不就是抽奖池嘛!!

    卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    目标:将 54 张牌打乱后,抽到区间 [1,10] 的概率为 40%,抽到区间 [11,20] 的概率为 20%,抽到区间 [21,30] 的概率为 20%,抽到区间 [31,40] 的概率为 15%,剩下的抽到 [41,54] 的概率为 5%;

    思路:我们实现乱序数组的同时,输出一个概率数组。

    radomNums = [21,43,12,3,...,8,6,33] // 共 54 项
    
    probabilityNums = [0.02,0.015,...,0.04,0.04,0.15] // 共 54 项,和为 1
    
    

    然后再通过以下 randomProbability 拿出发牌的值:

    function randomProbability(arr1, arr2) {
          var sum = 0,
          factor = 0,
          random = Math.random();
          for(let i = arr2.length - 1; i >= 0; i--) {
            sum += arr2[i]; // 统计概率总和
          };
          random *= sum; // 生成概率随机数
          for(let i = arr2.length - 1; i >= 0; i--) {
            factor += arr2[i];
            if(random <= factor) return arr1[i]; // 如果在当前的概率范围内,得到的就是当前概率,返回输出
          };
          return null;
    }
    
    const yourCard = randomProbability(radomNums, probabilityNums)
    
    console.log(yourCard)
    

    王者洗牌 ?

    此时,只见身在高处的王者背影。他缓缓转过身来。用手中的权杖指向了洗牌的终极的秘密:random 函数!神圣而又庄重!

    你可曾想过,前文处处在使用的 random 函数的机制是什么?

    如果我们破解了 random 函数的生成规则,是不是意味着破解了随机!

    ES5 对 random 函数是这样解释的:

    简单翻译就是 random() 函数产生的值在 [0,1) 之间,具有大致均匀的分布,不用带参数。

    进一步了解得知,在 Windows 系统 chrome V8 引擎下,浏览器最终调用一个 rand_s 函数,rand_s 是 Windows 系统提供的一个随机性足够强的随机数发生器。在其他系统下,也是基于当前系统获取高精度的随机数。所以,随机的根本源头是借助了操作系统的能力。

    但,MDN 上仍表示 Math.random 是一个伪随机数,它是不安全的。从 V8 的源码可以看到 Math.random 的值来源是 /dev/random,取 64 位,值的可能个数为 2 ^ 64,随机算法相对简单,只是保证尽可能的随机分布。

    2 的 64 次方还不够大吗?没错,它还不够!要知道 54 张扑克牌的全排列 54!(阶层)的值远远大于它。所以,它还不能覆盖大量的组合情景。

    那,这个世界上有 真随机数 吗?王者莞尔一笑~

    目前认为,真随机数需要从现实世界采集,比如 random.org 这个网站是通过采集大气噪音生成随机数。?

    还有:量子随机数,原理是 LED 发出随机数的光子,CMOS 捕获光源散粒噪声产生随机序列,量子芯片通过测量光量子态得到的随机数,再进一步来加密信息。

    不得不说,产生真随机数,是一件非常有意义的事情。至少在目前世界还未证实 P ≠ NP 的情况下,它是非常有意义的!!

    理想世界 ?

    有序和无序,或者说,熵增和熵减,是一个不小的难题。

    我们平常了解了那么多种排序算法,也理应了解洗牌算法(乱序算法)。

    设想下,如果你是上帝,在你设计的人类社会中,你想让人类这个群体时常感到困惑,迷茫,你会怎样打乱各种变因呢?无论人类怎么努力,都看不透你的打乱规则,这或许也只有上帝能做到吧~

    也许我们不断精确尺度的过程就是追寻认知上帝规则的过程......

    就像本瓜一直以为 1 秒钟就是 1 秒钟,而不知道它实际上被定义为:铯原子的 9192631770 次固有微波振荡次数所需的时间......

    ok,以上就是本次分享。有种设想,后续还会对有序无序、有理数无理数、P/NP 问题作更多探讨~

    卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)


    下载网 » 卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元