C++中的随机数
事情的开始是这样的,在大二的时候,写了几种排序算法,为了测试,就要为数组(或者容器)赋予一些随机初值,自然就用到了C/C++中的随机函数。
当时为了调用简单,将随机数赋值的过程写到了一个单独的函数里,这样一来,为数组(或容器)赋值就可以简洁高效。
但是,问题就是,按理来说,每次调用都该得到不同的随机数列,然而事实证明,“接连被赋值的数组”得到的随机数列却是一样的...
简述一下当时发现问题的地方:调用srand函数生成随机数序列,以当前计算机时间作为随机数种子,即srand((unsinged)time(NULL)),这个随机数初始容器函数是这样写的:
// 来自大二时候的Modnar,嗯,码写得很可爱... void Random(int list[], int n) { srand((unsigned)time(NULL)); for(int i = 0; i < n; i++) { list[i] = rand() % 100 + 1; } }
结果当一个函数(比如main中)调用这个初始化函数时,就会发生问题,比如这样调用:
int main(int argc, char *argv[]) { int lst1[20], lst2[20]; Random(lst1, 20); Random(lst2, 20); // Process... return 0; }
此时得到的lst1和lst2就会是相同的两个序列。
这里简单解释一下,因为time这个函数,传入的参数是一个指向需要被修改的对象的指针,返回值是计算机自起始时间起经过的时间(单位为秒),所以... 由于这个调用过程,时间用不到1s,自然两次调用时传入srand的数值是一样的,因此其生成的随机数序列就是一样的...
当然,这是现在了解了其中的原理后作出的解释,当时可爱的Modnar自然也有自己的想法,他是这样解释的:
嗯,为年轻而又热爱思考的Modnar点赞。当时的Modnar是利用这段码来验证想法的:
1 #include <time.h> 2 #include <stdlib.h> 3 4 #include <iostream> 5 #include <fstream> 6 7 #define N 20 8 9 using namespace std; 10 11 ofstream out; 12 13 int list[N]; 14 15 //最开始定义的random函数。 16 void Random1(int list[], int n) { 17 srand((unsigned)time(NULL)); 18 for(int i = 0; i < n; i++) { 19 list[i] = rand() % 100 + 1; 20 } 21 } 22 23 //代码同Random1()一样,而后发现其输出的序列同Random1()相同。 24 void Random2(int list[], int n) { 25 srand((unsigned)time(NULL)); 26 for(int i = 0; i < n; i++) { 27 list[i] = rand() % 100 + 1; 28 } 29 } 30 31 //最初试图通过先进行for空循环延时,来实现time返回值不同,后发现行不通(表明srand()功能实现的原理)。 32 void Random3(int list[], int n) { 33 srand((unsigned)time(NULL)); 34 int k = rand() % 100000000 + 1; 35 for(int i = 0; i < k; i++) { } 36 for(int i = 0; i < n; i++) { 37 list[i] = rand() % 100 + 1; 38 } 39 } 40 41 //通过前三个“随机”函数,发现了可以实现目标功能的随机函数。 42 void RandomFinal(int list[], int n) { 43 static int k = 0; //通过静态变量来找到上次调用rand()后,rand()的位置 44 srand((unsigned)time(NULL)); //随机数种子... 45 for(int i = 0; i < k * n; i++) { 46 rand(); //每次执行本函数,就先“跳过” k * n 个rand() 47 } 48 k++; 49 for(int i = 0; i < n; i++) { 50 list[i] = rand() % 100 + 1; //将跳过 k * n 个rand()后的随机序列赋值给list 51 } 52 } 53 54 void showlist(int list[], int n) { 55 for(int i = 0; i < n; i++) { 56 out << list[i] << " "; 57 } 58 out << endl; 59 } 60 61 int main() { 62 out.open("output.txt"); 63 if(!out) { 64 cout << "Error. Exit the program." << endl; 65 exit(0); 66 } else cout << "Open Succeessfully." << endl; 67 int locallist[N]; 68 out << "Local List:" << endl; 69 out << "First time:" << endl; 70 out << "Random1:" << endl; 71 Random1(locallist, N); 72 showlist(locallist, N); 73 out << "Random2:" << endl; 74 Random2(locallist, N); 75 showlist(locallist, N); 76 out << "Random3:" << endl; 77 Random3(locallist, N); 78 showlist(locallist, N); 79 80 out << "Second time:" << endl; 81 out << "Random1:" << endl; 82 Random1(locallist, N); 83 showlist(locallist, N); 84 out << "Random2:" << endl; 85 Random2(locallist, N); 86 showlist(locallist, N); 87 out << "Random3:" << endl; 88 Random3(locallist, N); 89 showlist(locallist, N); 90 out << endl; 91 92 out << "Global List:" << endl; 93 out << "First time:" << endl; 94 out << "Random1:" << endl; 95 Random1(list, N); 96 showlist(list, N); 97 out << "Random2:" << endl; 98 Random2(list, N); 99 showlist(list, N); 100 out << "Random3:" << endl; 101 Random3(list, N); 102 showlist(list, N); 103 104 out << "Second time:" << endl; 105 out << "Random1:" << endl; 106 Random1(list, N); 107 showlist(list, N); 108 out << "Random2:" << endl; 109 Random2(list, N); 110 showlist(list, N); 111 out << "Random3:" << endl; 112 Random3(list, N); 113 showlist(list, N); 114 115 out << endl << "FinalRandom:" << endl; 116 out << "Local List:" << endl; 117 out << "First time" << endl; 118 RandomFinal(locallist, N); 119 showlist(locallist, N); 120 out << "Second time:" << endl; 121 RandomFinal(locallist, N); 122 showlist(locallist, N); 123 out << "Third time:" << endl; 124 RandomFinal(locallist, N); 125 showlist(locallist, N); 126 127 out << endl << "Global List:" << endl; 128 out << "First time:" << endl; 129 RandomFinal(list, N); 130 showlist(list, N); 131 out << "Second time:" << endl; 132 RandomFinal(list, N); 133 showlist(list, N); 134 out << "Third time:" << endl; 135 RandomFinal(list, N); 136 showlist(list, N); 137 138 out.close(); 139 return 0; 140 }
其实我很懂Modnar的想法(23333,手动狗头),也就是说,其核心思想,就是srand会根据一个已有的巨大随机数序列来确定具体抽取哪段来作为函数返回(即所需的随机数序列)。
而且,当时的Modnar给出的解决方法是正确的(多么令人惊喜)!通过用static来“储存这时的随机数序列状态”。
如今,在略有了解std::C++11后,可以继续讨论一下这个问题(由于涉及到了一点儿很简单的自己写的东西,这里就不贴代码了):
在C++中,可以通过随机数引擎(相当于srand)来生成这个随机数序列,通过随机数分布(即图中的distribution)来进一步映射。
需要注意的是distribution的参数是随机数引擎,因为一些分布可能不只需要一个数值...(先这么理解吧,这儿不是主角)
下面是程序输出:
重点就是,可以看到,通过定义成static类型的引擎、分布对象,可以使得生成的随机序列不同!
(这里,不禁感叹当时Modnar的奇思妙想... 尽管这些东西翻翻书就能学到,哈哈哈哈)
这部分,在《C++ Primer 5th》有所提及:“一个给定的随机数发生器一直会生成相同的随机数序列。一个函数如果定义了局部的随机数发生器,应该将其(包括引擎和分布对象)定义为static的。否则,每次调用函数都会生成相同的序列。”(原书中文版第662页,英文版第748页)
书中强调的是直接使用引擎对象的情况下(即不给引擎初始时传入一个随机数种子),当然,上面已经分析过,尽管传入一个随机数种子(比如(unsigned)time(nullptr)这个种子值),往往会因为调用时间间隔太短而无法生效。
顺便说一下,在C++11中,对随机数的支持也趋渐完善,通过简单地调用随机数引擎以及不同类型的分布(例如正态分布等),也可以实现很多实用的数据生成功能,还是很值得一玩的。
当然,偶尔胡思乱想、任意猜猜也是挺有意思的,日后想起来,真的很深刻...(手动调皮)
@编辑于2019.3.7