javascript随机之洗牌算法深入分析


Posted in Javascript onJune 07, 2014

洗牌算法是我们常见的随机问题,在玩游戏、随机排序时经常会碰到。它可以抽象成这样:得到一个M以内的所有自然数的随机顺序数组。

在百度搜“洗牌算法”,第一个结果是《百度文库-洗牌算法》,扫了一下里面的内容,很多内容都容易误导别人走上歧途,包括最后用链表代替数组,也只是一个有限的优化(链表也引入了读取效率的损失)。

该文里的第一种方法,可以简单描述成:随机抽牌,放在另一组;再次抽取,抽到空牌则重复抽。
“抽到空牌则重复抽”这会导致后面抽到空牌的机会越来越大,显然是不合理的。
可以优化一步成:牌抽走后,原牌变少。(而不是留下空牌)
代码如下:

function shuffle_pick_1(m) //洗牌 //抽牌法
{
    //生成m张牌
    var arr = new Array(m);
    for (var i=0; i<m; i++) {
        arr[i] = i;
    }
    //每次抽出一张牌,放在另一堆。因为要在数组里抽出元素,把后面的所有元素向前拉一位,所以很耗时。
    var arr2 = new Array();
    for (var i=m; i>0; i--) {
        var rnd = Math.floor(Math.random()*i);
        arr2.push(arr[rnd]);
        arr.splice(rnd,1);
    }
    return arr2;
}

这个也明显有问题,因为数组如果很大的话,删除中间的某个元素,会导致后面的排队向前走一步,这是一个很耗时的动作。
回想一下“我们为什么要删除那个元素?”目的就是为了不产生空牌。
除了删除那个元素之外,我们是不是还有其它方式来去除空牌?
----有的,我们把最后一张未抽的牌放在那个抽走的位置上就可以了。
所以,这个思路我们可以优化成这样:

function shuffle_pick(m) //洗牌 //抽牌法优化牌
{
    //生成m张牌
    var arr = new Array(m);
    for (var i=0; i<m; i++) {
        arr[i] = i;
    }
    //每次抽出一张牌,放在另一堆。把最后一张未抽的牌放在空位子上。
    var arr2 = new Array();
    for (var i=m; i>0;) {
        var rnd = Math.floor(Math.random()*i);
        arr2.push(arr[rnd]);
        arr[rnd] = arr[--i];
    }
    return arr2;
}

除了抽牌思路,我们还可以用换牌思路。
《百度文库-洗牌算法》提到一种换牌思路:“随机交换两个位置,共交换n次,n越大,越接近随机”。
这个做法是不对的,就算n很大(例如10张牌,进行10次调换),也还存在很大可能“有的牌根本没换位置”。
顺着这个思路,做一点小调整就可以了:第i张与任意一张牌换位子,换完一轮即可。
代码如下:
function shuffle_swap(m) //洗牌 //换牌法
{
    //生成m张牌
    var arr = new Array(m);
    for (var i=0; i<m; i++) {
        arr[i] = i;
    }
    //第i张与任意一张牌换位子,换完一轮即可
    for (var i=0; i<m; i++) {
        var rnd = Math.floor(Math.random()*(i+1)),
            temp = arr[rnd];
        arr[rnd] = arr[i];
        arr[i]=temp;
    }
    return arr;
}

除了抽牌与换牌的思路,我们还可以用插牌的思路:先有一张牌,第二张牌有两个位置可随机插入(第一张牌前,或后),第三张牌有三个位置可随机插入(放在后面,或插在第一位,或插在第二位),依此类推
代码如下:

function shuffle_insert_1(m) //洗牌 //插牌法
{
    //每次生成一张最大的牌,插在随机的某张牌前。因为要在数组里插入元素,把后面的所有元素向后挤一位,所以很耗时。
    var arr = [0];
    for (var i=1; i<m; i++) {
        arr.splice(Math.floor(Math.random()*(i+1)),0,i);
    }
    return arr;
}

以上的代码也会有一些问题:就是随着牌数的增多,插牌变得越来越困难,因为插牌会导致后面的很多牌都往后推一步。
当然,我们也可以适当的优化一下:先有n-1张牌,第n张牌放在最后,然后与任意一张牌互换位置。
代码如下:

function shuffle_insert(m) //洗牌 //插牌法优化版,可以用数学归纳法证明,这种洗牌是均匀的。
{
    //每次生成一张最大的牌,与随机的某张牌换位子
    var arr = new Array(m);
    arr[0] = 0;
    for (var i=1; i<m; i++) {
        var rnd = Math.floor(Math.random()*(i+1));
        arr[i] = arr[rnd];
        arr[rnd] = i;
    }
    return arr;
}

好的,全部的代码如下,有兴趣的同学可以在自己的机器上试下,看下他们各自的执行效率、以及最后的结果是否是理论随机。

 

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>JK:javascript 洗牌算法 </title>
</head>
<body>
<script type="text/javascript">
function shuffle_pick_1(m) //洗牌 //抽牌法
{
    //生成m张牌
    var arr = new Array(m);
    for (var i=0; i<m; i++) {
        arr[i] = i;
    }
    //每次抽出一张牌,放在另一堆。因为要在数组里抽出元素,把后面的所有元素向前拉一位,所以很耗时。
    var arr2 = new Array();
    for (var i=m; i>0; i--) {
        var rnd = Math.floor(Math.random()*i);
        arr2.push(arr[rnd]);
        arr.splice(rnd,1);
    }
    return arr2;
}

function shuffle_pick(m) //洗牌 //抽牌法优化牌
{
    //生成m张牌
    var arr = new Array(m);
    for (var i=0; i<m; i++) {
        arr[i] = i;
    }
    //每次抽出一张牌,放在另一堆。把最后一张未抽的牌放在空位子上。
    var arr2 = new Array();
    for (var i=m; i>0;) {
        var rnd = Math.floor(Math.random()*i);
        arr2.push(arr[rnd]);
        arr[rnd] = arr[--i];
    }
    return arr2;
}

function shuffle_swap(m) //洗牌 //换牌法
{
    //生成m张牌
    var arr = new Array(m);
    for (var i=0; i<m; i++) {
        arr[i] = i;
    }
    //第i张与任意一张牌换位子,换完一轮即可
    for (var i=0; i<m; i++) {
        var rnd = Math.floor(Math.random()*(i+1)),
            temp = arr[rnd];
        arr[rnd] = arr[i];
        arr[i]=temp;
    }
    return arr;
}
function shuffle_insert_1(m) //洗牌 //插牌法
{
    //每次生成一张最大的牌,插在随机的某张牌前。因为要在数组里插入元素,把后面的所有元素向后挤一位,所以很耗时。
    var arr = [0];
    for (var i=1; i<m; i++) {
        arr.splice(Math.floor(Math.random()*(i+1)),0,i);
    }
    return arr;
}
function shuffle_insert(m) //洗牌 //插牌法优化版,可以用数学归纳法证明,这种洗牌是均匀的。
{
    //每次生成一张最大的牌,与随机的某张牌换位子
    var arr = new Array(m);
    arr[0] = 0;
    for (var i=1; i<m; i++) {
        var rnd = Math.floor(Math.random()*(i+1));
        arr[i] = arr[rnd];
        arr[rnd] = i;
    }
    return arr;
}

//alert(shuffle_pick(10))

var funcs = [shuffle_pick_1, shuffle_pick, shuffle_swap, shuffle_insert_1, shuffle_insert],
    funcNames = ["抽牌", "抽牌优化", "换牌", "插牌", "插牌优化"]
    m = 10000,
    times=[];
for(var i = 0; i < funcs.length; i++){
    var d0= new Date();
    funcs[i](m);
    funcNames[i] = (new Date() - d0) + '\t' + funcNames[i];
}
alert(funcNames.join('\n'));
</script>

</body>
</html>
Javascript 相关文章推荐
在网页中屏蔽快捷键
Sep 06 Javascript
javascript得到XML某节点的子节点个数的脚本
Oct 11 Javascript
jquery UI 1.72 之datepicker
Dec 29 Javascript
JQUERY操作JSON实例代码
Feb 09 Javascript
js猜数字小游戏的简单实现代码
Jul 02 Javascript
js获取当前月的第一天和最后一天的小例子
Nov 18 Javascript
jquery中EasyUI实现异步树
Mar 01 Javascript
javascript继承的六大模式小结
Apr 13 Javascript
详解vue express启动数据服务
Jul 05 Javascript
angular $watch 一个变量的变化(实例讲解)
Aug 02 Javascript
ES6 javascript的异步操作实例详解
Oct 30 Javascript
中高级前端必须了解的JS中的内存管理(推荐)
Jul 04 Javascript
ECMAScript6的新特性箭头函数(Arrow Function)详细介绍
Jun 07 #Javascript
js中的caller和callee属性介绍和例子
Jun 07 #Javascript
javascript中拼接HTML字符串的最快、最好的方法
Jun 07 #Javascript
javascript在网页中实现读取剪贴板粘贴截图功能
Jun 07 #Javascript
JavaScript异步回调的Promise模式封装实例
Jun 07 #Javascript
jQuery的缓存机制浅析
Jun 07 #Javascript
Firefox中使用outerHTML的2种解决方法
Jun 07 #Javascript
You might like
?繁体转换的class
2006/10/09 PHP
Windows服务器中PHP如何安装redis扩展
2019/09/27 PHP
PHP实现简单用户登录界面
2019/10/23 PHP
网页中实现浏览器的最大,最小化和关闭按钮
2007/03/12 Javascript
javascript实现的使用方向键控制光标在table单元格中切换
2010/11/17 Javascript
jquery.cookie.js 操作cookie实现记住密码功能的实现代码
2011/04/27 Javascript
JavaScript数组常用操作技巧汇总
2014/11/17 Javascript
jQuery实现仿微软首页感应鼠标变化滑动窗口效果
2015/10/08 Javascript
AngularJS中如何使用$http对MongoLab数据表进行增删改查
2016/01/23 Javascript
Radio 单选JS动态添加的选项onchange事件无效的解决方法
2016/12/12 Javascript
js验证手机号、密码、短信验证码代码工具类
2020/06/24 Javascript
Cookies 和 Session的详解及区别
2017/04/21 Javascript
深入理解vue Render函数
2017/07/19 Javascript
js禁止表单重复提交
2017/08/29 Javascript
使用selenium抓取淘宝的商品信息实例
2018/02/06 Javascript
vue操作动画的记录animate.css实例代码
2019/04/26 Javascript
在Python的Flask框架中使用日期和时间的教程
2015/04/21 Python
使用python生成目录树
2018/03/29 Python
python 字典操作提取key,value的方法
2019/06/26 Python
Python实现一个带权无回置随机抽选函数的方法
2019/07/24 Python
解决Python pip 自动更新升级失败的问题
2020/02/21 Python
python实现图像全景拼接
2020/03/27 Python
深入了解Python装饰器的高级用法
2020/08/13 Python
torchxrayvision包安装过程(附pytorch1.6cpu版安装)
2020/08/26 Python
英国最大的纸工艺品商店:CraftStash
2018/12/01 全球购物
餐饮收银员岗位职责
2014/02/07 职场文书
职业生涯规划书结束语
2014/04/15 职场文书
我的长生果教学反思
2014/04/28 职场文书
电话客服专员岗位职责
2014/06/28 职场文书
安全生产年活动总结
2014/08/29 职场文书
优秀学生干部事迹材料
2014/12/24 职场文书
办公室岗位职责范本
2015/04/11 职场文书
八月迷情观后感
2015/06/11 职场文书
网络舆情信息简报
2015/07/21 职场文书
python字符串的一些常见实用操作
2022/04/06 Python
Nginx代理Redis哨兵主从配置的实现
2022/07/15 Servers