杂谈

对C语言随机数相关的一些不足之处进行优化

前言

在探究C语言随机数之前,我们应该知道,C语言的随机数是伪随机,“伪随机”看上去是随机的,但其实是确定的。伪随机通常利用一个随机数种子,通过某些特定的算法,生成固定的数字序列,也就是说只要输入的随机数种子相同,获得的随机数序列就是相同的。


C语言随机数种子的改进

在C语言中,我们写随机数通常会调用srand()与rand()两个库函数
srand()用于设定随机数种子,rand()用于返回随机数

在学习中,我们了解到srand()可以调用time函数作为随机数种子

srand((unsigned)time(NULL));

time函数能够返回从1970年1月1号到至今所走过的时间

我们写一个简单的程序来检测一下这个随机数种子的特性

time间隔.png

#include<stdio.h>
#include<windows.h>
#include<time.h>
int main()
{
    while(1)
    {
        srand((unsigned)time(NULL));//循环生成种子
        int a = rand();
        printf("%d\n",a);
        Sleep(200);//等待0.2秒
    }
}

通过输出数据可以发现,time函数的刷新频率是1秒刷新一次,并且生成的第一个随机数永远在递增

  • 改进

随机数种子过低的刷新频率在某些特定情况下会导致一些奇怪的问题,因此我们可以采用其他刷新频率更快的函数作为随机数种子

例如:

srand(GetTickCount());

该函数能够返回自设备启动后的毫秒数,是精度更高的随机数种子
该函数只能在windows环境下运行,需要windows.h头文件

gettimecount间隔.png

#include<stdio.h>
#include<windows.h>
#include<time.h>
int main()
{
    while(1)
    {
        srand(GetTickCount());
        int a = rand();
        printf("%d\n",a);
        Sleep(1);
    }
}

可以看到该函数刷新频率为1毫秒一次,精度远高于time函数,因为可以使用GetTickCount()代替time()


C语言随机数生成的改进

我们在使用随机数时,通常希望每个数出现的概率是均等的,但C语言的rand()函数在这方面并非如此
我们做一个测试
我们利用rand()函数生成大量0-9999之间的数字

如果生成的数据在0-999,则tmp[0]自增一次
生成的数据在1000-1999,则tmp[1]自增一次
生成的数据在2000-2999,则tmp[2]自增一次

以此类推,总共分成10部分
利用这个程序来统计随机数散布在各个区间的数量,验证随机数每个数字被生成的概率是否均等

代码如下

#include<stdio.h>
#include<windows.h>

int main()
{
    srand(GetTickCount());//设置随机数种子
    int a = 0;
    int tmp[10];
    for(int i = 0;i < 10;i++)
    {
        tmp[i] = 0;
    }
    for(int i = 0; i< 1000000;i++)
    {
        a = rand()%10000;//生成0-9999
        a/=1000;
        tmp[a]++;
     } 
    for(int i = 0;i < 10; i++)
    {
        printf("tmp[%d] = %d\n",i,tmp[i]);
    }
    return 0;
}

rand概率.png

经过验证,发现前面3个部分的概率与后面7个部分的概率出现严重偏差

C语言rand()的返回值范围为 [0, RAND_MAX]。在Windows环境下,RAND_MAX 一般为32767。也就是说,对于
rand() % 10000 来说,如果取到 [0, 2767]
这个范围的值,那么有四种情况:0xxxx、1xxxx、2xxxx、3xxxx,概率为 3/32767 ;而如果取到 [2768, 9999]
这个范围的值,就只有三种情况:0xxxx、1xxxx、2xxxx,概率为 4/32767

作者:SuperSodaSea
链接:https://www.zhihu.com/question/62091492/answer/194529253 来源:知乎

综上,当取值为[0,2767]时,概率会比其他数字低25%的概率
12*0.75=9,与我们的验证数据相符

因此,为了解决这个问题,我参考别人的思路写了一个改进函数

即求 [a,b] 区间随机数时取定一个 (b-a+1) 的倍数 m,使 m < RAND_MAX 且最大;然后在 rand() 返回值超过
m 时舍弃,不超过 m 时选用并取模;最后结果加 a 得到 [a,b] 上的随机数。如果 RAND_MAX 不够大,需要把两次 rand()
的结果拼起来当成 RAND_MAX+1 进制的大数使用。

int rand_(int min,int max)
{
    int mult = RAND_MAX/max;//计算最大公约数 
    int num = 0;
    do{
        num = rand();
    }
    while(num>max*mult);//如果生成的数字超过了最大公倍数,那么重新生成一个 
    return num%max+min;
}

new.png

经过验证,随机数偏差问题得到基本解决

This is just a placeholder img.