一个随机数引起的血案

一个随机数引起的血案

我也来作一次标题党
原文地址:http://blog.csdn.net/WinsenJiansbomber/article/details/50604653javascript

目录

原由

事情是这样的,刚不久在CSDN上看到有ASK.CSDN.NET,就点了进来,发现CSDN也来搞百度知道这类东东。因而乎就点了一些问题来看,其中有一条,是关于随机数应试题的。html

一个编程面试题,只要写出伪代码就能够了。试题内容:假设有以下两个函数 rand3()能够产生随机的0 1 2,rand5()能够产生随机的0 1 2 3 4,如今请你利用它编写一个函数rand7(),产生0~6的随机数。java

问题还不是这个应试题,要命的是参考答案。在看到 ask.csdn.net 这个问题以前,我从未以为随机数是这么好玩的东西。先不说 csdn 玩 ask 有点炒 codeproject 冷饭的感受,就凭看到这个问题的一些花花绿绿的答案,就以为国内国外的程序从业人员水平差距真不是通常小。web

问题根源

其实,这个问题有点像是代码复用范围的问题,题目已经给出 rand3 和 rand5 这两个程序所依赖的功能。做答者只需根据条件来重组它,获得问题所要求的逻辑。万万没想到的是,出现的答案居然是 for while 随便用的,两层 while 还不够的境况!有些代码则差很少能够写成一篇独立的文章了。面试

因此,从这些现象看来,彷佛做答者都是忘记了题目,在侃大山的。若是真是这样,倒也还好,起码显露得不是真正的水平。编程

从原理上来说,这是高中的组合排列问题。这个问题,我记得 Robert 在《the Art and Science of C》这本书中以骰子举过生动的例子。一个六面的骰子,随机出现任一面的几率都是1/6。抛两次,将会有36种状况,将两个结果相加为0、12的状况只有一种,就是两次都一样跌出0、6。相比相加结果是6这种状况,两次抛出的骰子能够是0+6,6+0,1+5,5+1,2+4,4+2,3+3等6种状况。所以它们结果的几率是程一个单峰山形的分布,中间高,两边低。canvas

特别地,当抛的次数越多,则相邻的两个数的几率差异的变化就会变得相对缓慢。所以,经过屡次求和再求模的方式,能够必定程度将几率变得更均匀,甚至是能够认为是均衡的,但从原理上它们是不平均的。这一点对于本题来说,是比较诡异的一个特色。下面以统计数据的方式来讲明,以5次rand3相加为例,一次实测的数据是这样的:浏览器

1 ->  2013  2.01%
2 ->  6177  6.18%
3 -> 12325 12.33%
4 -> 18404 18.40%
5 -> 21045 21.05%
6 -> 18641 18.64%
7 -> 12394 12.39%
8 ->  6121  6.12%
9 ->  2070  2.07%
10 ->   411 0.41%

数据能够看到经过直接的相加获得的是几率分布是差别明显的,若是经过求模的方式来求取 0-5 的几率分布,则会出现惊人的效果:dom

1 ->  2013  2.01% + 7 -> 12394 12.39% = 14.40%
2 ->  6177  6.18% + 8 -> 6121 6.12% = 12.30%
3 -> 12325 12.33% + 9 -> 2070 2.07% = 14.40%
4 -> 18404 18.40% + 10 -> 411 0.41% = 18.81%
5 -> 21045 21.05% = 21.05%

结果显示,几率分布已经明显地趋于平均分布,由于求模过程产生了互补效应,只要设置的求模点恰当,几率的分布是能够接近平均分布的。svg

对应方案

那么,说了这么多,参考答案有些什么内容呢,下面就连同我专门为随机数这个问题写的一个测试程序一同奉上:

<!DOCTYPE>
<html> <head> <title> random test </title> <meta name="link" content="http://ask.csdn.net/questions/234228" /> <meta name="generator" content="editplus" /> <meta name="author" content="Jimbowhy" /> </head> <body> <div id="canvas"></div> <script type="text/javascript"> //此处放入javascript代码 </script> </body> </html>
function rand3(){
      return ~~(Math.random()*3);
  }
  function rand5(){
      return ~~(Math.random()*5);
  }
  function output(name, a, k){
      var canvas = document.getElementById("canvas");
      canvas.innerHTML += "<h3>"+name+ " " + k +"</h3>";
      for(var i in a){
        var v = a[i];
        canvas.innerHTML += i + " -&gt; " + v + " " + (v/k*100).toFixed(2) + "%<br>";
      }
  }
  function test(target, k){
      if(!target) return;
      var a = [];
      for(var i=0; i<k; i++){
          var j = target();
          !a[j]? a[j] = 1 : a[j]++;
      }
      output("Test "+target.name,a,k);
  }

  function rand7(){
    var sum=0;
    sum+=rand3();//0,1,2
    sum+=rand3()+3;//3,4,5
    sum+=rand3()+6;//6,7,8
    sum+=rand5()+9;//9,10,11,12,13
    sum%=7;//0,1,2,3,4,5,6
    return sum;
  }//代码很具备迷惑性,可是将代码化简一下就是:
   //sum=3*rand3()+rand5()+18!这已经面目全非,再求模也是枉然啊!
  function rand7f(){
    return rand3()+rand3()+rand3();
  }//单峰形,一眼看去,好象三个 rand3 各自独立互不影响,
   //其实出现0、6的几率是远低于出现3的几率的。
   //一眼看上去好像很正确,其实它是典型的单峰形,中间的数出现几率最高,
   //两边的就最低。无数据,根本想不到它们有多离谱
  function rand7e(){
    var sum = 0;
    for(var i=0;i<7;i++) {
        sum += rand5() + i*5;
    }
    return sum%7;
  }//0-34等几率数对7求模,近似等几率OK,只是 i*5 没有存在的意义,
  //由于5+10+...+30=105,105%7=0
  function rand7d(){
    var sum = 0;
    for (var i=0;i<7;i++) {
        sum+= rand3()+i*3;
    }
    return sum%7;
  } // 双峰形
  function rand7c(){
    var sum;
    switch(rand3())
    {
    case 0:
        sum=rand5();//0,1,2,3,4
        break;
    case 1:
        sum=rand5()+5;//5,6,7,8,9
        break;
    case 2:
        do
        {
            sum=rand5()+10;//10,11,12,13,14
        }while(sum!=14);
        break;
    }
    sum%=7;//0,1,2,3,4,5,6
    return sum;
  }//递减形
  function rand7b(){
      return rand3() + rand5()
  }//中峰形
  function rand7a(){
    var x5 = rand5();
    var x3 = rand3();
    while (x5 == x3 ) {
        x3 = rand3();
    }
    return x5+x3;
  }//高低起伏形
  test(rand5, 9999);
  test(rand7, 99999);
  test(rand7a, 99999);
  test(rand7b, 99999);
  test(rand7c, 99999);
  test(rand7d, 99999);
  test(rand7e, 99999);
  test(rand7f, 99999);

将以上内容保存为HTML文件,并用浏览器如CHROME打开它,就能够看到运行结果了。

更新

关于rand7c,ysuwood修改了while条件,我在分析TA的答案时没有注意到,所以要从新分析:

回复ysuwood:
这回看到了,要再分析一下。若是while==14,就直接将0的几率截掉一大块了。代码近乎实用,可是还有个问题,你们可能没留意到:case 0 - 2 的几率分布是平均,也就是说,出现0-4和5-9,还有10-14的几率是同样的,那么求模后的结果好像就是几率平均分布的!其实否则,当while条件中把14废了以后,rand5出现14的几率还在,此时只好从新抽,这,就是这里,意味着出现14的几率转接到了10-13这几个数上,因此这几个数不明显地比0-9这几个数的几率要高。之因此说不明显,是由于通过了求模的运算,它有平滑几率的做用。@caozhy 好像也没注意到这个状况。

为了验证这个分析,能够将rand7c中的求模运算注解掉,再运行,这样结果就会显示0-13这几个数的几率分布了。

再有,在评论TA的另外一个答案时,硬是生生地把几行代码错误地简化成了一条语句,原代码是这样的:

sum+=rand3();//0,1,2
sum+=rand3()+3;//3,4,5
sum+=rand3()+6;//6,7,8
sum+=rand5()+9;//9,10,11,12,13

化简过程在这里:

Jimbo 回复ysuwood: 哦,还真的是,我掉坑里了,SORRY,SORRY,SORRY,误导看官。可是三个相加也不是正确的作法。

ysuwood 回复Jimbo: 随机数能相加和乘3等价吗?糊涂了吧。

Jimbo: 代码很具备迷惑性,可是将代码化简一下就是:sum=3*rand3()+rand5()+18!这已经面目全非,再求模也是枉然啊!

另外 @lovingning 提供了一个解答问题的正确思路:

lovingning: 第一枚骰子有三种可能,第二枚骰子有五种可能,排列组合有十五种可能,取其中的七种或十四种,剩下的抛弃不就好了,只要保证七分之一或者十四分之一,反正是电脑运行,有什么资源浪费的

电脑运行是其次的,主要问题是几率要平均,这是重点。

参考