🎲 随机数试验
Conmajia © 2012, 2018
Published on May 23rd, 2012
Updated on March 10th, 2018
这是重新排版的版本。
真随机和伪随机
随机数是计算机编程中一个非常重要的工具. 随机数够不够随机,非常关键. 在软件编程中,比如C♯(其他任何语言均与此类似),可以用 System.Random
来获得随机数. 从本质上讲,Random
生成的不是真随机数,而仅是具有一定随机程度的伪随机数. Wikipedia 上对伪随机数的解释是这样的.
伪随机数,或称伪乱数,是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机。在计算伪随机数时假如使用的开始值不变的话,那么伪随机数的数序也不变。伪随机数的随机性可以用它的统计特性来衡量,其主要特征是每个数出现的可能性和它出现时与数序中其它数的关系。伪随机数的优点是它的计算比较简单,而且只使用少数数值很难推算出计算它的算法。一般人们使用一个假的随机数,比如电脑上的时间作为计算伪随机数的开始值。
几乎所有的真随机数发生器都具有专利保护. 下图列举了部分真随机数发生器专利.
理论上,真随机数只有可能在自然界中由物理现象产生,必须由硬件支持实现.
System.Random
原理
System.Random
基于Donald E. Knuth[1] 的减随机数生成器算法实现,从实用角度而言,随机程度已经足够. 加上 Random
可以利用种子进行随机化,在日常使用中并无不妥.
关于试验
这次试验,来自于我偶然想到一种情况. 假设计算机飞速发展,而软件又只能生成伪随机数,那么现在的一般应用情况:
Random rand;
while (true) {
// 用种子初始化随机数
rand = new Random (seed);
int value = rand.Next ();
// (费时的操作)
}
在未来超高速计算环境下,现在认为费时的操作也许就一晃而过,用时几乎可以忽略. 在这样的极限情况下,上面的语句就变成了单纯的循环生成随机数. 那么 Random
生成的数值,还能保证一定程度的随机性吗?
试验方法
模拟最严酷的环境,使用类似下面这样的代码,通过改变不同的 rand
生成方式和 count
数,对多种 Random
生成方式及相应的随机数生成结果进行对比.
// 示例代码,不可用
for (int i = 0; i < count; i++) {
rand.Next ();
}
试验条件[2]
项目 | 配置 |
---|---|
CPU | i3 380m 2.56GHz 2核4线程 |
内存 | DDR3 1066 2GB x2 |
硬盘 | TOSHIBA MK3265GSX (320 GB) @ 5400 RPM |
运行时 | .NET Framework版本:v2.0.50727(C♯ 2.0) |
开发环境 | Visual Studio 2005 Team Suite |
操作系统 | Windows XP SP3 |
测试用例
这次试验针对以下 8 种常见的随机数生成方式进行测试:
-
默认构造函数
使用Random rand=new Random()
. 单次生成,不重复. 这也是 MSDN 建议的生成方式. -
默认构造函数(重复创建)
仍然使用默认构造函数,但是在每次循环中都重新创建Random
变量. -
常数种子
使用rand=new Random(10)
. 用常数作为种子. -
常数种子(重复创建)
类似用例 #2. -
时间刻度种子
这是比较普遍的一种用法,每次利用系统当前时间作为种子. 使用DateTime.Now.Ticks
. 注意只能重复创建,否则将成为 #3. -
随机数种子
这是一种看起来很随机的方法,使用另一个Random
变量,在每次循环时用Next()
作为种子. 注意只能重复创建,否则将成为 #3. -
三角函数种子
使用Math.Sin()
作为种子. -
复杂种子
这是种子随机性比较大的一种方法,利用 GUID 哈希值、当前时间Ticks
和计数器相乘来计算种子. 有很多高手推荐.
private static int randomCount = 0;
private static string CreateRandomText () {
randomCount++;
Guid guid = Guid.NewGuid ();
int key1 = guid.GetHashCode ();
int key2 = unchecked ((int)DateTime.Now.Ticks);
int seed = unchecked (key1 * key2 * randomCount);
Random rand = new Random (seed);
int n = rand.Next (100000, 999999);
// (业务代码,略)
}
试验原理
统计在生成范围内,生成的多个随机数的分布情况,然后在 PictureBox
中绘制出统计直方图. 同时,程序将统计最大重复出现次数和随机数跳变次数,以检验 Random
的生成结果变化情况. 跳变数越多,表示生成的数字变化得越多,反之则生成数字变化较少. 最理想的情况是跳变次数等于生成次数,即每次生成的数值都不同.
试验程序的实现和代码和本文无关,不作说明.
试验结果样张说明
下面是两张测试样张,分别记为 A 和 B.
这是测试生成100个随机数的结果. 生成的随机数范围是沿\(x\)轴从左到右,整个样图的Width
范围. Random
每生成一个随机数\(r\),即在\(x=r\)处的\(y\)轴上\(+1\),因此直方图每条线高度代表该线所在坐标数值出现次数.
图中文字说明含义为:
- 最大重复 同样的数值出现的最大次数,例如生成100个随机数\((0,1,2,\cdots)\),其中1出现了60次,则最大重复为60
- 跳变 当某次生成的随机数和前次不同,则跳变数\(+1\)
- 用时 生成全部随机数所用时间,该时间不含界面渲染花费的时间
需要注意的是直方图的高度是自适应的,因此两张不同的直方图对比时需要参考每张图上标注的最大重复。
如何阅读样张
测试程序运行后得到的样张参见前一节的 Sample A 和 Sample B. 样图记录的是 Random
生成的随机数的重复次数统计,因此越是不重复的样图,表示该测试用例的随机性越高. 在图上看,就是直方图线条越多越好,最大重复数值越小越好,跳变越大越好.
试验结果
每张测试结果样张中均包含全部 8 个测试用例,注意观察图中的数据.
结果分析
#1 默认构造函数
默认构造函数单次创建的Random
变量忠实的完成了近乎白噪声[3]的随机数输出分布.
#2 默认构造函数(重复创建)
很差的随机性. 也展示出一个问题,即同样种子创建的 Random
,其输出具有极高的相似性. 这和伪随机序列有关,参见 #4.
#3 常数种子
使用常数完成初始化的 Random
,和 #1类似,实现了相当程度的随机性输出.
#4 常数种子(重复创建)
每次采用同样的种子创建 Random
,并取 Random
生成的第一个随机数,得到的数值完全一样. 这充分说明了计算机生成的伪随机序列,如果种子一样,序列index
一样,那么得到的值也是几乎一样的. 证明了 Random
获取的并非真实随机数.
#5 时间刻度种子
对于高强度的生成,以时间为种子的 #5 却出现了随机不起来的问题. 仔细分析,以 100 万次重复为例,共消耗时间 3757.835 毫秒,平均每次消耗即 3757 纳秒. DateTime.Ticks
的最小时间刻度为100纳秒,照理说不应该出现种子来不及变化的情况. 把每次创建 Random
时的时间种子 dump 出来观察,会发现平均每 20~50 次循环后,DateTime.Now.Ticks
才会发生变化. 这导致输出的值域收缩了 20~50 倍.
#6 随机数种子
用 #1的方式生成的随机数,以此作为新 Random
的种子,随机性上得到了保证,达到白噪声分布的效果.
#7 三角函数种子
使用 Math.Sin()
作为种子. 由于 \(\sin(x)\in[-1,1]\),所以需要乘以某常数进行放大. 放大带来的问题就是值域的收缩,再加上 \(\sin(x)\) 的周期性,出现了种子取值固定的问题.
#8 复杂种子
让人寄予厚望,运算复杂,非常专业的 #8 终于不负众望,获得了和 #1、#3 同样的噪声结果.
总结
消耗众多资源,反复计算得到的 #6、#8 用例,在实际结果上,和 #1、#3 是相似的,因为白噪声已经是随机数能达到的最大性能. 显然,这样大量高强度产生随机数的情况下,最简单有效的办法,就是使用 #1 中最简单的单次默认构造函数创建 Random
变量.
以上即是本人通过试验结果做出的简单推论,仅供参考.
The End. \(\Box\)
if(jQuery('#no-reward').text() == 'true') jQuery('.bottom-reward').addClass('hidden');