取样问题——《编程珠玑》读书笔记
问题描述:
给定一个足够大的随机选择函数rand(), 对于[0, n)范围内的数,随机选取m个(m<n),且取出的m个数有序排列(增序或减序)。
问题解答:
1、 一开始拿到问题的时候,我的想法就是从rand()%n中取出m个不同的数。这其中需要解决的问题一个就是如何去重,另一个是排序问题。
作者的算法参考自《The Art of Computer Programming. Volume2: Seminumerical Algorithms》,考虑方向完全不一样。
该算法依次考虑整数0,1,2……n-1,并通过一个适当的随机测试对每个整数进行选择。通过按序访问整数,可以保证输出结果的有序性。
为了理解选择的标准,我们考虑下m=2, n=5的情况。那么我们需要对每个整数的选取概率定为2/5,并且要保证一定能选出2个数出来。
首先我们会考虑整数0,选择0的概率为2/5,可以通过下面的语句来实现:
if (rand() % 5) < 2
不幸的是,我们不能用同样的概率来选择整数1:这样做的话我们从5个整数中选出的整数可能是两个也可能不是两个。因此决策有一些不同:在已经选择0的情况下以1/4的概率选择1, 而在没有0的情况下一2/4的概率选择1(我们来看下选择1的概率是多少:2/5 * 1/4 + 3/5 * 2/4 = 2/5,使我们需要的概率)。一般说来,如果要从r个剩余的整数中选出s个,我们以概率s/r选择下一个数。
下面是完整的C代码,可参考
#include <iostream> #include <stdlib.h> using namespace std; int genknuth(int m, int n) { for (int i = 0; i < n; i++) { if ((rand() % (n - i)) < m) { cout << i << "\t"; m--; } } cout << endl; return 0; } int main() { genknuth(10, 101); return 0; }
2、顺带附上刚开始的想法,时间复杂度为O(mlogm):
int gensets(int m, int n) { set<int> s; while (s.size() < m) { s.insert(rand() % n); } set<int>::iterator i; for (i = s.begin(); i != s.end(); i++) { cout << *i << "\t"; } cout << endl; return 0; }
3.
生成随机数另一种方法是把包含整数0~n-1的数组顺序打乱,然后把m个元素排序输出。Ashley Shepherd和Alex Woronow发现,在这个问题中我们只需要打乱数组的前m个元素,时间复杂度为O(n)
int intcmp(const void* it1, const void* it2) { return *(int*)it1 - *(int*)it2; } int randrange(int a, int b) { return (rand() % (b -a)) + a; } int genshuf(int m, int n) { int i, j; int* x = new int[n]; for (i = 0; i < n; i++) { x[i] = i; } int t; for (i = 0; i < m; i++) { j = randrange(i, n - 1); t = x[i]; x[i] = x[j]; x[j] = t; } qsort(x, m, sizeof(int), intcmp); for (i = 0; i < m; i++) { cout << x[i] << "\t"; } cout << endl; return 0; }