首页 > 代码库 > 一个随机数发生器(一)

一个随机数发生器(一)

老黄、老银和老阳共事宇宙中心某互联网企业H,并且同在一个由校招新生组成的Team里。刚走出校园的他们,对自己去年在校招这场演技大比拼中的表现意犹未尽,因此常会拿一些没什么实际意义的技术问题大谈特谈。

这次是午饭后的扯皮时间,这几个人就吸着免费的明胶酸奶,从一些概率问题讨论到了随机数发生器上……

 

黄:像去年(校招),面试官就很喜欢问这种题目:给定一个[0, M)的均匀分布随机整数发生器(以下简称RNG),设计一个[0, N)的整数的均匀分布RNG。

阳:我被问的时候不知道怎么做,问我的人就得意的告诉我用——M进制。

黄:现实中RNG一般输出的是什么呢?

阳:我觉得最基本的应该是以bit或者byte为单位输出的。毕竟RNG就是一连串的位操作,比如像那个什么Twister算法(梅森旋转算法)这种,所以可见M必定是2的幂。

黄:如果是这样,我会先用RNG产生足够的随机位数存下N,然后截取小于N的值返回就行了。

阳:……我想如果从这个RNG出发,它只有类似于nextByte的方法返回一个随机byte,以此实现一个生成指定范围的int、long、float、double的RNG……

黄:int和long很简单啊,就我刚才说的方法

老黄在白板上写下pseudo code

int nextInt(int min, int max) {    int range = max - min;    while (true) {        int random = 0;        int ceiling = 1;        for (int i = 0; i < 4; ++i) {            random = (random << 8) | rng.nextByte();            ceiling = ceiling << 8;            if (ceiling >= range)                break;        }        if (random < 0 || random >= range)            continue;        return min + random;    }}

(看上去不错,首先把[a, b)的问题转化为[0, b-a);生成随机数时,只产生了必要的byte数;最后考虑了负数情况)

阳:……貌似有两个问题:1)range有可能溢出int的范围;2)在极端情况下,这个RNG生成一个随机数平均要执行200多次nextByte。比方说给定随机数范围[0, 1)吧,其实只有0是可取的,你这边虽然只调用一次nextByte,但是值域是[0, 256),取到0的概率是1/256啊,这是不是太heavy了?……哦,还有一个问题,3)ceiling只要算一次就行了吧?而你这边生成失败就会重算。

黄:第一个问题range可以用long替代。第二个问题么……

阳:那nextLong怎么办?

黄:……有没有128位的整型?

阳:…………我想没有……你可以造一个…………算了,我们限定所有的范围都必须是非负整数吧,那第二个问题呢?

黄:(思考一分钟)……我改变一下设计。

int nextInt(int min, int max) {    int range = max - min;    int times = 0;    int ceiling = 1;    for (; times < 4; ++times) {        if (ceiling >= range)            break;        ceiling = (ceiling << 8);    }    if (ceiling < 0)        ceiling = MAX_INT;    ceiling = ceiling - ceiling % range;    while (true) {        int random = 0;        for (int i = 0; i < times; ++i)            random = (random << 8) | rng.nextByte();        if (random < 0 || random >= ceiling)            continue;        return min + random % range;    }}

黄:现在我只需执行一次就能获得最小byte数times,然后ceiling现在是小于等于RNG产生值域的最大值的range的倍数中最大的那个……

阳:我明白,你把[0, ceiling)上的均匀分布折叠到[0, range)上,只要ceiling是range的倍数,均匀性就能保持,ceiling扩大了RNG输出的有效值域范围,也就降低了重复试验的次数。

黄:对,换而言之,重复试验的概率现在最差情况是1/2,就是当range刚好超过RNG输出值域一半的时候,这时候range本身就是ceiling……但这里面还有一个问题:当range非常大,需要四次nextByte的时候,我有1/2概率会得到负数,加上刚才说的情况,于是我会有3/4的概率要抛弃生成的byte,而这正好又发生在nextByte调用次数最多的一类里面。

阳:可以用abs?

黄:对,但是要排除掉MIN_INT,否则会溢出嘛…………(沉思片刻)………不对,不行。

阳:为什么?

黄:你看,排除掉MIN_INT我得到的是[-MAX_INT, MAX_INT]上的均匀分布?

阳:对。

黄:然后我用abs把负数对折到正数上?

阳:…………我明白了,是0,对折后,0取到的概率是其他正整数的1/2,这就破坏均匀性了。

黄:是的…………其实我们只要这样,把负数的最高位置零就行了,这建立了负数到非负数的一一映射。

阳:MIN_INT被映射到了0,不错。但均匀性能保证么,比如虽然是一一映射,但是把松散的区域映射到密集的区域就不一定有效吧?

黄:应该不会,首先nextByte是无符号的,所以通过它生成的4字节随机数是无符号整型上的均匀分布,uint上大于MAX_INT的部分也就是[MIN_INT, -1]并且顺序也是一样的,所以我们把负数高位1抹去,也就是一种折叠,这种操作是证明保持均匀性的。相比较abs反而不合理,因为它将[MIN_INT+1, -1]上分布先逆序,再叠加到正整数上,我不确定这种操作是否合理。

阳:有道理。

黄:所以代码这么写。

int nextInt(int min, int max) {    int range = max - min;    int times = 1;    int ceiling = 1;    for (; times < 4; ++times) {        if (ceiling >= range)            break;        ceiling = (ceiling << 8);    }    if (ceiling < 0)        ceiling = MAX_INT;    ceiling = ceiling - ceiling % range;    while (true) {        int random = 0;        for (int i = 0; i < times; ++i)            random = (random << 8) | rng.nextByte();        random = random & MAX_INT;        if (random < ceiling)            return min + random % range;    }}

阳:现在最坏情况下期望2次可以得到随机数。

黄:其实现在我觉得没必要获得times,每次都生成一个4字节随机数也可以,毕竟省掉了计算times的一次循坏。

int nextInt() {    int random = 0;    for (int i = 0; i < 4; ++i)        random = (random << 8) | rng.nextByte();    return random & MAX_INT;}int nextInt(int max) {    int ceiling = MAX_INT - MAX_INT % max;    while (true) {        int random = nextInt();        if (random < ceiling)            return min + random % range;    }}int nextInt(int min, int max) {    return min + nextInt(max - min);}

黄:写成这样可读性更好不是么,也能丰富API。

阳:现在这样实现,long的写法也一模一样了,不错。下面看看浮点数怎么实现。

银:你们可以看看apache.commons.lang模块啊,人家的……好像叫RandomUtils吧,什么都有。可以学学别人的实现。

(未完)

 

一个随机数发生器(一)