疯狂位图之——位图扩展随机数

  这篇文章主要介绍随机数,不过这次讲述的随机数是从上两篇的延伸出来的,所以还是继续疯狂位图的标题,欢迎看完本文拍砖。

一、实现需求

  利用C语言提供的rand()函数构造一个随机函数randLong(),能够生成0-(2^31-1)之间的随机整数。rand()生成的随机整数在0-(2^15-1)范围内。可以参考上一篇的位图生成数据。

二、利用位图的方法

  起初我以为很简单:

void randLong(){
    return 1.0*rand()/RAND_MAX*LONG_MAX;
}

  后来受五岳的文章启发,发现我犯了严重的错误,rand()的取值空间中只有2^15-1=32767个整数,按上面的方法只是在0-(2^31-1)上抽取了一小部分数。如果说rand()生成的是0-32767直接的随机浮点数而不是整数的话,上面的办法扩展随机数的范围是可行的。

  因为一直在整位图,所以在想能不能从位入手,实现随机数的扩展。我都有点感受到位图的强大了,还真想出了一个用位图扩展的方法,这里不再拐弯抹角,直接上菜:

  需要生产的随机数是[0-(2^31-1)]的闭区间内,端点值化成2进制后为:

0: 0000,0000,0000,00000000,0000,0000,0000
     ...
     ...
     ...
2^31-1:0111,1111,1111,1111,1111,1111,1111,1111 

  看到这个二进制后,相信你已经有想法了。我们申请一个32位长整型的位图,将位图每一位初始化为0,这时候我们需要一个rand1()生产0,1的随机数,对每个位调用一次random=rand1(),如果random=1就把该位设置为1,如果为random=0则设置为0,调用31次(除最高位为0不需要随机选择)后,这个长整型变量的值就是我们需要的随机数。

  如果rand1()足够随机,即等概率(1/2)的生成0、1,那么我们按上面的方法的得到的任意一个数的概率都为(1/2)^31。来看一下实现代码:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

/*******生成0、1的随机产生器********/
short rand01(){
    return rand() % 2;
}
/********将bit所在位设置为1**********/
void setOne(long * p, short bit){
    *p |= 1<<bit;
}
/********将bit所在位设置0**********/
void setZero(long *p, short bit){
    *p &=~(1<<bit);
}
/*******生成0-2^bitNum-1之间的随机数**************/
long randLong(short bitNum){
    long a = 0;
    long *p = &a;    
    for(short i= 0;i < bitNum; i++){
        short bit = rand01() % 2;
        if(bit == 0)
            setZero(p,i);
        else
            setOne(p,i);
    }
    return *p;
}

  randLong(short bitNum)可以生成0到2的任意幂次方之间的随机数(当然bitNum<=31),生成的随机数基本上等概率的。我们可以测试一下生成0-7的随机数概率:

void main(){
    const int max = 8;
    int count[max];
    for(short i=0;i<max;i++)
        count[i] = 0;
    long num=10000000;    
    for(long i=0;i<num;i++){
        long index = randLong(3);//生成0-(2^3-1)的随机数
        count[index]++;//计数    
    }
    for(short i=0;i<max;i++)
        printf("%d:\t%lf\n",i,1.0*count[i]/num);//输出概率    
}

  输出的结果如下: 

  进行了1000万次试验,发现每个数的概率还是很接近1/8=0.125的。

三、递归实现的方法

  上面有位图的方法,每次生成一个随机数需要调用多次rand01(),速度略微慢些,这里再提供一个基于递归的实现方法,直接上代码:

/**********根据rand()构造[0,max]的闭区间的随机整数**********/
long random(long max){
    if(max <= RAND_MAX)//RAND_MAX是rand()的最大值,值为0x7fff
        return rand()% (max+1);//直接得到[0,max]的随机数
    else
        return (random(max/(1+RAND_MAX)) * (1+RAND_MAX) + rand()) % (max+1);//递归得到[0,max]的随机数
}

    当max<=RAND_MAX时,对rand()模余就得到了[0,max]的随机数;

  当max>RAND_MAX时,我们先来看 (random(max/(1+RAND_MAX)) * (1+ RAND_MAX) + rand()) ,令k=max/(1+RAND_MAX),即k表示max是(1+RAND_MAX)的倍数向下取整,前面的式子变为(random(k) * (1+RAND_MAX)+ rand())。这里为了方便说明,我们假设rand()生成[0,4]的整数(即RAND_MAX=4),max=52,于是k=52/(1+4)=10,相当于把[0,52]分成10个区间,根据定义random(10)生成的是[0,10]的整数,random(10)*5相当于随机的定位到每个区间的开始位置,再加上一个rand(),即在左端点上加一个随机的偏移量构成一个新的随机数,而10依然大于4,需要用同样的方法生成random(10)。可以结合图来理解:  

  最后还需要将(random(10) * 5+ rand())对53模余,因为两个相加的结果可能会大于52,模余53可以保证生成的数在[0,52]范围内,注意是模余53不是52。

  有了上面的方法我们可以进一步改进,生成任意区间上的随机整数,代码如下:

/*******根据c语言提供的rand()生成long型的任何区间随机整数值*******/
long random(long min,long max){
    if(max <= RAND_MAX)
        return min + (rand()%(max - min + 1));
    else
        return min + ((random(max/(1+RAND_MAX)) * (1+RAND_MAX) + rand()) % (max-min+1));
}

   我们来测试一下区间随机数:

void testRandom(){
    long min = 234567;
    const short num = 9;
    long count[num+1];
    for(short i=0;i<=num;i++){
        count[i]=0;
    }
    long testTimes = 10000000;//试验次数
    for(long i=0;i<testTimes;i++)
        count[random(min,min+num)-min]++;//计数器
    for(short i=0;i<=num;i++)
        printf("%d:\t%lf\n",i+min,1.0*count[i]/testTimes);//输出概率    
}

 生成了[234567,234567+9]之间的10个数,运行的结果如下:

  每个数出现的概率非常接近1/10=0.1,可以认为是等概率生成。

  前面我们说过,模余可以保证生成的数不超过范围内,但会造成概率不相等的情况,特别是在用大的rand()生成小的随机数时,我们测试一下,用rand7()构造rand4(),rand7()生成[0,7]的整数,rand4()生成[0,4]的整数,看如下代码:

int rand7(){//生成0到7的整数
    return rand()%8;
}
int random4(){//生成0到4的整数
    return rand7() % 5;
}
void main(){
    const int max = 5;
    int count[max];
    for(short i=0;i<max;i++)
        count[i] = 0;
    long num=10000000;    
    for(long i=0;i<num;i++){
        count[random4()]++;//计数    
    }
    for(short i=0;i<max;i++)
        printf("%d:\t%lf\n",i,1.0*count[i]/num);//输出概率
}

  输出的结果如下:

  

  我们可以看到进行1000万次试验,生成0、1、2的概率约为0.25=1/4,而3、4的概率约为0.125=1/8,这两组之间的概率相差一倍,为什么会这样?稍加分析,我们就知道了,rand7()生成的数为[0,1,2,3,4,5,6,7],且是等概率的生成这8个数,模余5后的结果为[0,1,2,3,4,0,1,2],所以rand4()中的0,1,2即有可能来自[0,1,2,3,4,5,6,7]中的0,1,2,也可能来自5,6,7,而rand4的3,4只能由rand7()的3,4而来。所以会出现0,1,2的概率是3,4概率的两倍,一般的测试实验可以不用考虑,在需要严格的随机测试实验时,应当注意这个问题。用到模余的地方都有可能造成概率不等的情况,要想写一个通用的概率相等的random(m,n)是一个数学难题。

  挂羊头卖狗肉了,这篇主要讲述随机数的一些东西,也牵扯到了些位图。

  补充:后来想到用位运算扩展C语言的rand()到long范围内的randLong(),其实可以用位移运算,只需要调用rand()就可以了,看代码:

/*********通过移位来构造长整型的随机数***************/
long randLong(){
    long r = (long)rand();
    return (r << 16)+ rand();
}

   这个比调用31次都rand01()快很多,而且只要rand()是等概率的,生成的randLong()也是等概率的。

  感谢关注,欢迎评论。

  转载请注明出处:http://www.cnblogs.com/fengfenggirl

  

posted @ 2013-07-03 09:43  CodeMeals  阅读(1300)  评论(0编辑  收藏  举报
reliable statistics
Visitors