1

你真的懂随机数?

Author : Jasper Yang
School : Bupt

Q:为什么要写这篇文章?
A:因为我发现在最近的科学计算中,常常遇到随机数,所有的随机数都是基于0,1随机,而这个0,1随机怎么实现呢?下面我会娓娓道来~

这篇文章不同于网路上的杂散的技术文,我是针对 random 这么一个论题展开调研最后将所有相关的知识进行整理叙述,希望每个人看完都可以得到小小的提升~

& 什么是随机数

随机数:数学上产生的都是伪随机数,真正的随机数使用物理方法产生的

随机数种子:随机数的产生是由算术规则产生的,在c++中,srand(seed)的随机数种子不同,rand()的随机数值就不同,倘若每次的随机数种子一样,则rand()的值就一样。所以要产生随机数,则srand(seed)的随机数种子必须也要随机的。在 python 中就是 random.seed()来设置种子。

下面我讲的随机数不仅仅讲随机数生成的原理,也会讲在python中以及在c++中怎么去实现,当然,大部分资料也都是网上找的,我只是做了一个整理汇总,并用自己的语言加以叙述。

& 随机数的原理

这里我看了一篇博客,由于这篇博客是那个博主转的,但是该博主并没有表明是从哪里转来的,我就不po出链接了,大家往下看~

有位朋友问那博主关于一段程序的错误。

C/C++ code
for (int i =0;i< n;++i)
{
    srand((unsigned)time( NULL )); 
    int r = rand()%100;
    cout << r << ",";
}

这里很明显他是想输出一串小于100的随机的数列.可是运行结果输出的却是类似 97,97,97,97,....97,30,30,30,30,30,30,30,30,30,30,30,30,....,27,27,27,27,27,27,....的序列.很明显这样完全看不出有任何的随机性.这是由于他对C的rand函数不理解导致的错误用法.而这两天逛C#区我也同样看到了几个类似的错误用法(C和C#的rand从大体的原理上差不多).想想自己初学的时候类似的错误犯得也不少.所以自己下去查了写资料总结了在随机数使用上的一些错误的用法.希望能对初学者有所帮助。

现在各种语言中的随机数产生函数所产生的"随机数",实际上被称之为"伪随机数".可以将
整个随机数函数看做这样一个表达式:

$$A = R(s)$$

其中R是随机函数,s是种子.A是一个数列.即对于任意一个种子s,经过R的计算后,总有一个确定的数列A与之对应.而当在C#里调用var rnd = new Random (s)或在C里调用srand(s)实质上所做工作之一就是设定这个种子.而rnd.Next();或rand()只不过是在A上取下一个元素而已.当然实际的实现不可能事先计算一个数列A,所以rand()相当于由s计算出下一个数字s',然后将s'作为新的种子赋值给s,最后将s'作为结果返回。

往细了讲,就是这样。

如果约定:$a_1=f(seed),a_{n+1}=f(an)$
那你可以行到一个序列:$a_1,a_2,a_3...a_n$,那么要制作一个伪随机函数rand,只需要让它每调用一次就返回序列的下一个元素就行。

下面是两种常见的错误做法

C# code
for (int i=0;i<n;++i)
{
    var rnd = new Random (s);//s是实先确定的一个数字
    Console.Write ("{0},",rnd.Next());
}

这样,每次使用Random,都去申请了一个变量rnd,然后才用这个变量去找随机数(rnd.Next())。这样其实就是在随机数的序列中总是在找第一个。这样下来,第一个数肯定是固定的,就不存在什么随机数了。

第二种情况更加常见。

C# code
for (int i=0;i<n;++i)
{
    var rnd = new Random ();//用系统时间作为种子
    Console.Write ("{0},",rnd.Next());
}

之前的第一种情况使用了一个固定的常数s来做种子,这里选用了系统时间做种子,想要达到随机的效果,但是得到的结果往往就会是和博主那位朋友一样的结果97,97,97,97,....97,30,30,30,30,30,30,30,30,30,30,30,30,....,27,27,27,27,27,27,.... 。

这是因为Windows系统时钟的更新频率大概在10ms左右.而这个for循环的执行显然要快
得多.于是在一段执行时间内Environment.TickCount (Random的默认种子)或是C的time函数返回的都是同一个值.从而导致rnd.Next在一段时间内返回一个常数。

所以正确的做法应该是把种子移出循环之外。

C# code
var rnd = new Random ();//用系统时间作为种子
for (int i=0;i<n;++i)
{
    Console.Write ("{0},",rnd.Next());
}

各种库中是怎么实现随机数呢?
在 Linux 下实现的方式类似如下

static unsigned long next = 1;

/* RAND_MAX assumed to be 32767 */
int myrand(void) {
    next = next * 1103515245 + 12345;
    return((unsigned)(next/65536) % 32768);
}

void mysrand(unsigned seed) {
    next = seed;
}

myrand、mysrand分别对应rand和srand,但实际的rand实现会复杂一些。

下面是这位博主实现的方式,其实挺简单的,我们每个人都可以实现一种自己想要的随机数方式加到自己的私有库中~

*
 * Copyright (c) 2008 Microsoft::Tsorgy.Utils, Reserved.
 * 
 * Filename:    @(#)Random.cs
 * Create by:   TsOrgY
 * Email:       tsorgy@gmail.com
 * Date:        2008/12/27 15:01:40
 * 
 * Classname:   Random
 * Description: 一种能够产生满足某些随机性统计要求的数字序列的设备.
 *              
 */
using System;
using System.Runtime.InteropServices;
namespace Tsorgy.Utils {
    /// <summary>
    /// 表示伪随机数生成器,一种能够产生满足某些随机性统计要求的数字序列的设备.
    /// </summary>
    [Serializable]
    [ComVisible(true)]
    public class Random {
        private int inext;
        private int inextp;
        private const int MBIG = 0x7fffffff;
        private const int MSEED = 0x9a4ec86;
        private const int MZ = 0;
        private int[] SeedArray;
        /// <summary>
        /// 使用与时间相关的默认种子值,初始化 Random 类的新实例.
        /// </summary>
        public Random()
            : this(Environment.TickCount) {
        }
        /// <summary>
        /// 使用指定的种子值初始化 System.Random 类的新实例.
        /// </summary>
        /// <param name="Seed">用来计算伪随机数序列起始值的数字。如果指定的是负数,则使用其绝对值。</param>
        /// <exception cref="System.OverflowException">Seed 为 System.Int32.MinValue,在计算其绝对值时会导致溢出。</exception>
        public Random(int Seed) {
            this.SeedArray = new int[0x38];
            int num2 = 0x9a4ec86 - Math.Abs(Seed);
            this.SeedArray[0x37] = num2;
            int num3 = 1;
            for (int i = 1; i < 0x37; i++) {
                int index = (0x15 * i) % 0x37;
                this.SeedArray[index] = num3;
                num3 = num2 - num3;
                if (num3 < 0) {
                    num3 += 0x7fffffff;
                }
                num2 = this.SeedArray[index];
            }
            for (int j = 1; j < 5; j++) {
                for (int k = 1; k < 0x38; k++) {
                    this.SeedArray[k] -= this.SeedArray[1 + ((k + 30) % 0x37)];
                    if (this.SeedArray[k] < 0) {
                        this.SeedArray[k] += 0x7fffffff;
                    }
                }
            }
            this.inext = 0;
            this.inextp = 0x15;
            Seed = 1;
        }
        private double GetSampleForLargeRange() {
            int num = this.InternalSample();
            if ((((this.InternalSample() % 2) == 0) ? 1 : 0) != 0) {
                num = -num;
            }
            double num2 = num;
            num2 += 2147483646.0;
            return (num2 / 4294967293);
        }
        private int InternalSample() {
            int inext = this.inext;
            int inextp = this.inextp;
            if (++inext >= 0x38) {
                inext = 1;
            }
            if (++inextp >= 0x38) {
                inextp = 1;
            }
            int num = this.SeedArray[inext] - this.SeedArray[inextp];
            if (num < 0) {
                num += 0x7fffffff;
            }
            this.SeedArray[inext] = num;
            this.inext = inext;
            this.inextp = inextp;
            return num;
        }
        /// <summary>
        /// 返回非负随机数.
        /// </summary>
        /// <returns>大于或等于零且小于 System.Int32.MaxValue 的 32 位带符号整数。</returns>
        public virtual int Next() {
            return this.InternalSample();
        }
        /// <summary>
        /// 返回一个小于所指定最大值的非负随机数.
        /// </summary>
        /// <param name="maxValue">要生成的随机数的上界(随机数不能取该上界值)。maxValue 必须大于或等于零。</param>
        /// <returns>大于或等于零且小于 maxValue 的 32 位带符号整数,即:返回的值范围包括零但不包括 maxValue。</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">maxValue 小于零。</exception>
        public virtual int Next(int maxValue) {
            if (maxValue < 0) {
                throw new ArgumentOutOfRangeException("maxValue", string.Format("'{0}' must be greater than zero.", maxValue));
            }
            return (int) (this.Sample() * maxValue);
        }
        /// <summary>
        /// 返回一个指定范围内的随机数.
        /// </summary>
        /// <param name="minValue">返回的随机数的下界(随机数可取该下界值)。</param>
        /// <param name="maxValue">返回的随机数的上界(随机数不能取该上界值)。maxValue 必须大于或等于 minValue。</param>
        /// <returns>一个大于或等于 minValue 且小于 maxValue 的 32 位带符号整数,即:返回的值范围包括 minValue 但不包括 maxValue。如果minValue 等于 maxValue,则返回 minValue。</returns>
        /// <exception cref="System.ArgumentOutOfRangeException">minValue 大于 maxValue。</exception>
        public virtual int Next(int minValue, int maxValue) {
            if (minValue > maxValue) {
                throw new ArgumentOutOfRangeException("minValue", string.Format("'{0}' cannot be greater than {1}.", minValue, maxValue));
            }
            long num = maxValue - minValue;
            if (num <= 0x7fffffffL) {
                return (((int) (this.Sample() * num)) + minValue);
            }
            return (((int) ((long) (this.GetSampleForLargeRange() * num))) + minValue);
        }
        /// <summary>
        /// 用随机数填充指定字节数组的元素.
        /// </summary>
        /// <param name="buffer">包含随机数的字节数组。</param>
        /// <exception cref="System.ArgumentNullException">buffer 为 null。</exception>
        public virtual void NextBytes(byte[] buffer) {
            if (buffer == null) {
                throw new ArgumentNullException("buffer");
            }
            for (int i = 0; i < buffer.Length; i++) {
                buffer[i] = (byte) (this.InternalSample() % 0x100);
            }
        }
        /// <summary>
        /// 返回一个介于 0.0 和 1.0 之间的随机数.
        /// </summary>
        /// <returns>大于或等于 0.0 而小于 1.0 的双精度浮点数字。</returns>
        public virtual double NextDouble() {
            return this.Sample();
        }
        /// <summary>
        /// 返回一个介于 0.0 和 1.0 之间的随机数.
        /// </summary>
        /// <returns>大于或等于 0.0 而小于 1.0 的双精度浮点数字。</returns>
        protected virtual double Sample() {
            return (this.InternalSample() * 4.6566128752457969E-10);
        }
    }
}

这里我要另外提到一个大家听到了很多次的东西 ------------> 线性同余法

这也是实现随机数的一种方式

线性同余方法(LCG)

它的递归公式:

$$N_{j+1} = (A * N_j +B) (mod M)$$

其中A,B,M是产生器设定的常数。

LCG的周期最大为M,但大部分情况都会少于M。要令LCG达到最大周期,应符合以下条件:

  1. B,M互质

  2. M的所有质因子的积能整除A-1

  3. 若M是4的倍数,A-1也是

  4. A,B,$N_0$都比M小

  5. A,B是正整数

最后生成的就是一个 <$N_i$> 序列,这个序列应该满足下面的几个条件。

  1. 这个函数应该是一个完整周期的产生函数。也就是说,这个函数应该在重复之前产生出0 到m之间的所有数

  2. 产生的序列应该看起来是随机的

  3. 这个函数应该用32bit 算术高效实现

实现

#include <stdio.h>  
#include <time.h>  
static unsigned long rand_seed;  
void mysrand (unsigned long int);  
void myrand ();  
int  
main (void)  
{  
    int i;  
  
    mysrand (time (NULL));  
    for (i = 0; i < 100; i++)  
      {  
          myrand ();  
      }  
    return 0;  
}  
  
void  
mysrand (unsigned long seed)  
{  
    rand_seed = seed;  
}  
  
void  
myrand ()  
{  
    rand_seed = (rand_seed * 16807L) % ((1 << 31) - 1);  
    printf ("%ld ", rand_seed);  
}  

可以看到,这个实现和上面提到的 linux 的实现很像,其实就是一样的。

& 随机数使用

因为最近用的c++和python特别的多(我觉得这两个语言是程序员们最需要掌握的两种语言,别的都是补充 ~:)),所以下面我就只讲这两种语言的实现方式。

c++

实例程序

#include "stdafx.h"
#include <time.h>
#include <stdlib.h>
int _tmain(int argc, _TCHAR* argv[])
{
 // 初始化随机数种子
 // time函数返回从1970年1月1日零时零分零秒到目前为止所经过的时间,单位为秒
 srand((int)time(NULL));
 int j;
 for (int i = 0; i < 10; i++) {
  j = (rand() * 10) / RAND_MAX + 1; // 生成1~10之间的随机数
  printf("j = %d \n", j);
 }
 unsigned start = (rand() * 1000)/ RAND_MAX + 15550; // 生成15550~16549之间的随机数
 printf("start = %d \n", start);
 start &= ~1; // 把start变为偶数,如果是奇数,则start变为start - 1的偶数
 printf("start = %d \n", start);
 getchar();
 return 0;
}

c++ 其实就是 srand 和 rand 两个函数。
上面的都只是生成的整数,如果需要浮点数什么的就需要自己再加以处理,而在python中提供了比较多的函数。

python

这块的内容是 Capricorn的实验室的整理。其实这块内容直接去官网的doc翻译就可以了,但是我有点懒,不太想去看了,就用了这篇博文的内容~

<h3>random.random</h3>
random.random()用于生成一个0到1的随机符点数: 0 <= n < 1.0

<h3>random.uniform</h3>

random.uniform的函数原型为:random.uniform(a, b),用于生成一个指定范围内的随机符点数,两个参数其中一个是上限,一个是下限。如果a>b,则生成的随机数n: a <= n <= b。如果 $a<b$, 则 b <= n <= a。

<h3>random.randint</h3>

random.randint()的函数原型为:random.randint(a, b),用于生成一个指定范围内的整数。其中参数a是下限,参数b是上限,生成的随机数n: a <= n <= b。

<h3>random.randrange</h3>

random.randrange 的函数原型为:random.randrange([start], stop[, step]),从指定范围内,按指定基数递增的集合中 获取一个随机数。如:random.randrange(10, 100, 2),结果相当于从[10, 12, 14, 16, ... 96, 98]序列中获取一个随机数。random.randrange(10, 100, 2)在结果上与 random.choice(range(10, 100, 2) 等效。

<h3>random.choice</h3>

random.choice从序列中获取一个随机元素。其函数原型为:random.choice(sequence)。参数sequence表示一个有序类型。这里要说明 一下:sequence在python不是一种特定的类型,而是泛指一系列的类型。list, tuple, 字符串都属于sequence。有关sequence可以查看python手册数据模型这一章。下面是使用choice的一些例子:

print random.choice("学习Python")   
print random.choice(["JGood", "is", "a", "handsome", "boy"])  
print random.choice(("Tuple", "List", "Dict"))  

<h3>random.shuffle</h3>

random.shuffle的函数原型为:random.shuffle(x[, random]),用于将一个列表中的元素打乱

<h3>random.sample</h3>

random.sample的函数原型为:random.sample(sequence, k),从指定序列中随机获取指定长度的片断。sample函数不会修改原有序列。

OK,告一段落了~,朋友们,有没有觉得进步了一点点呢~

paper done 2017/05/13

jasperyang
203 声望58 粉丝

Highest purpose is Hacking...