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

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

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

青铜洗牌

题目:给你一副崭新的扑克牌(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 次!因为越往后,生成随机数重复的概率就越大!

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

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

关键词:【交换位置】

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

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

王者权重检测分析(从青铜到王者玩转)(1)

代码实现:

// 生成 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 位替换(即替换自身);

王者权重检测分析(从青铜到王者玩转)(2)

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

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

王者权重检测分析(从青铜到王者玩转)(3)

代码实现:

// 生成 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),才是永远滴神!

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

王者权重检测分析(从青铜到王者玩转)(4)

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

王者权重检测分析(从青铜到王者玩转)(5)

研究表明:用鸽尾式洗牌法【洗七次】是最有效的打乱手法!(谁研究的?自行 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]

大师洗牌

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

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

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

王者权重检测分析(从青铜到王者玩转)(6)

目标:将 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 函数是这样解释的:

15.8.2.14 random ( )

Returns a Number value with positive sign, greater than or equal to 0 but less than 1, chosen randomly or pseudo randomly with approximately uniform distribution over that range, using an implementation-dependent algorithm or strategy. This function takes no arguments.

简单翻译就是 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!(阶层)的值远远大于它。所以,它还不能覆盖大量的组合情景。

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

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

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

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

理想世界

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

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

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

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

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

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

王者权重检测分析(从青铜到王者玩转)(7)

我是掘金安东尼: 一名人气前端技术博主(文章 100w 阅读量)

终身写作者(INFP 写作人格)

坚持与热爱(简书打卡 1000 日)

我能陪你一起度过漫长技术岁月吗(以梦为马)

觉得不错,给个点赞和关注吧(这是我最大的动力 )b( ̄▽ ̄)d

,