《编程珠玑》笔记12 随机数生成
1.问题
抽象后的问题如下:输入两个整数m和n,(m < n).输出0~n-1范围内的m个随机整数的有序列表,不允许重复。
也就是说,要对0~n-1范围内的数字进行选择,每个数字被选中的概率相等.
有两点要注意:不允许重复,结果有序;
2.解决方案
2.1已有知识
利用库函数<stdlib.h>中的rand()函数可以产生0到RAND_MAX范围内的随机整数。
RAND_MAX是在前面头文件中定义的宏,具体大小与实现有关,至少为32767(2^15-1).
一般32位机,int型为4字节,故RAND_MAX大小为2147483647(2^31-1).
两个相关函数:
产生很大随机整数bigrand():RAND_MAX *rand() + rand();实际上就是先产生前15位,再产生后15位,也即rand()<<15 | rand();
产生指定范围随机整数 randint(l,u): rand()%(u-l+1)+l; 产生的数据在[l,u]之间.
(后面对时间复杂度的讨论,都默认rand()需要单位时间)
2.2方法一:使用set集
这种方法也是一种较为通用的方法,使用set不仅排除了生成相同的数字,对其遍历的过程也实现了排序。
void getsets(int m, int n) { set<int> S; while(S.size() < m) S.insert(rand()%n); //insert保证了如果出现S中已有数字,那就什么都不做 set<int>::iterator i; for(i = S.begin(); i != S.end(); i++) cout << *i << endl; }
时间效率:set每次插入时间为O(logm),while循环总共进行了m次插入,遍历集合需要O(m).故总时间为O(mlogm).额外的空间需求为set集的大小:O(m).
2.3方法二:Knuth的算法S
依次考虑整数 0,1,2,……,n-1,通过一个适当的随机测试对每个整数进行选择。通过按序访问整数,保证输出结果有序。
设m=2, n=5,那么选择第一个整数0的概率就是2/5,这种确定概率,通过 if (rand()%5) < 2 来判断是否选取该数字,然后判断是否选择整数1,若0被选中,以 1/4 的概率选择1, 若0未被选中,以 2/4 的概率选择1,…… ,总之,对于从r个剩余整数中选s个,以 s/r 来选择下一个数。
void getknuth(int m, int n) { for(int i = 0; i < n; i++) if(rand() % (n-i) < m) { cout << i << endl; m--; } }
时间效率:该程序的时间复杂度是与n有关的:O(n),空间上只需要几十个字节。
2.4方法三:打乱数组顺序
对于包含整数0~n-1的数组,打乱数组的前m个元素,然后把前m个元素排序输出即可。
void genshuf(int m, int n) { vector<int> x(n); for(int i = 0; i < n; i++) x[i] = i; for(int j = 0; j < m; j++) { int k = randint(i, n-1); SWAP(x[i], x[k]); } sort(x, x+m); for(int j = 0; j < m; j++) cout << x[j] << endl; }
时间效率:时间上为O(n+mlogm), 一次初始化和排序。空间上也需要O(n).
2.5其他方法
根据问题的实际情况(m和n的相对大小),如若n为100万,m为n-10时,可以生成10个元素的随机样本,然后输出不在样本中的整数
3.原理
解决问题的步骤:理解分析问题 —— 建模提出抽象模型 —— 考虑多种解法 —— 实现一种方案 —— 回顾改进
4.习题
4.8 从0~n-1中随机选择m个数,
输出顺序随机,不重复:(使用方法2.4,但不进行排序)
fori = [0,n)
x[i] = i;
for j = [0,m)
int k = randint(i, n-1);
SWAP(x[i], x[k]);
for i = [0,m)
cout << x[i]
输出顺序随机,可重复:(最普通的情况)
for i = [0, m)
cout << rand() % n;
输出有序,不重复:
前面写的三种方法均可。
输出有序,可重复:
删除对生成数字是否已在结果集的判断,(可以采用第13章将要介绍的各种数据结构,也可以直接使用multiset标准STL)