问题
本章研究的问题是取样问题,也就是程序设计中的随机数,问题描述如下:
程序的输入包含两个整数m和n,其中 m < n;输出是0~n-1范围内m个随机整数的有序列表,不允许重复。从概率的角度看,我们希望没有重复的有序选择,其中每个选择出现的概率相等。
条件假设:
我们假设有一个能返回很大的随机整数(远远大于m 和 n )的函数bigrand(),以及一个能返回i…j范围内均匀选择的随机整数的randint(i,j)。
本章关于这个问题提供了三种算法,接下来详细叙述每个算法的程序实现。
算法1
该算法依次考虑0,1,2,…,n-1 , 并通过一个适当的随机测试对每个整数进行选择。通过按序访问整数,我们可以保证输出结果是有序的。
代码实现如下:
/************************************************************************/
/* 《编程珠玑》第十二章 取样问题
* 问题:程序的输入包含两个整数m和n,其中m<n。输出是0~n-1范围内m个随机整数的有序列表,不允许重复
* 方案一
*/
/************************************************************************/
#include <iostream>
#include <algorithm>
#include <cstdlib>
using namespace std;
/************************************************************************/
/* 返回一个很大的随机整数(远大于m和n) */
/************************************************************************/
int bigRand()
{
return RAND_MAX * rand() + rand();
}
/************************************************************************/
/* 返回一个位于l与u之间的均匀选择的随机整数 */
/************************************************************************/
int randint(int l , int u)
{
return l + bigRand() % (u - l + 1);
}
/************************************************************************/
/* 解决问题的算法1 */
/************************************************************************/
void rand1(int m, int n)
{
int select = m, remaining = n;
for (int i = 0; i < n; i++)
{
if ((bigRand() % remaining) < select)
{
cout << i << "\t";
select--;
}
remaining--;
}
cout << endl;
}
int main()
{
int m = 5, n = 10;
while (cin >> n >> m)
{
rand1(m, n);
}
system("pause");
return 0;
}
对于该算法,只要m<=n,程序选出的整数就恰好为m个,不会选择更多的整数,因为select变为0的时候,就不能选择整数了;也不会选择更少的整数,因为select/remaining为1的时候,一定会选中一个整数。以上代码中,我们可以看出,每个子集被选中的概率是相同的。
对于该算法,程序实现只需要占用几十个字节的内存,而且可以快速解决问题。但是,当n很大的时候,代码运行就是相对较慢。
算法2
我们知道,C++标准程序库中的集合set有两个重要性质,集合内元素不重复,集合内元素有序排列(默认升序)。对于求随机数的问题,可以利用set的性质,向一个初始为空的set中插入随机整数知道数量达到要求为止。
算法实现如下:
/************************************************************************/
/* 《编程珠玑》第十二章 取样问题
* 问题:程序的输入包含两个整数m和n,其中m<n。输出是0~n-1范围内m个随机整数的有序列表,不允许重复
* 方案二
*/
/************************************************************************/
#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <set>
using namespace std;
/************************************************************************/
/* 返回一个很大的随机整数(远大于m和n) */
/************************************************************************/
int bigRand()
{
return RAND_MAX * rand() + rand();
}
/************************************************************************/
/* 返回一个位于l与u之间的均匀选择的随机整数 */
/************************************************************************/
int randint(int l, int u)
{
return l + bigRand() % (u - l + 1);
}
/************************************************************************/
/* 解决问题的算法2 */
/************************************************************************/
void rand2(int m, int n)
{
set<int> s;
while (s.size() < m)
{
s.insert(bigRand() % n);
}
set<int>::iterator iter;
for (iter = s.begin(); iter != s.end(); iter++)
cout << *iter << "\t";
cout << endl;
}
int main()
{
int m = 5, n = 10;
while (cin >> n >> m)
{
rand2(m, n);
}
system("pause");
return 0;
}
C++标准模板库规范每次插入操作都在O(logm)的时间内完成,而遍历集合则需要O(m)时间,因此完整的程序需要O(mlogm)时间。但是该数据结构空间开销比较大。
算法3
生成随机整数的有序子集的另一种方法是把包含整数0~n-1的数组顺序打乱,然后把前m个元素排序输出。
算法实现如下:
/************************************************************************/
/* 《编程珠玑》第十二章 取样问题
* 问题:程序的输入包含两个整数m和n,其中m<n。输出是0~n-1范围内m个随机整数的有序列表,不允许重复
* 方案三
*/
/************************************************************************/
#include <iostream>
#include <algorithm>
#include <cstdlib>
using namespace std;
/************************************************************************/
/* 返回一个很大的随机整数(远大于m和n) */
/************************************************************************/
int bigRand()
{
return RAND_MAX * rand() + rand();
}
/************************************************************************/
/* 返回一个位于l与u之间的均匀选择的随机整数 */
/************************************************************************/
int randint(int l, int u)
{
return l + bigRand() % (u - l + 1);
}
/************************************************************************/
/* 解决问题的算法3 */
/************************************************************************/
void rand3(int m, int n)
{
int *x = new int[n];
for (int i = 0; i < n; i++)
{
x[i] = i;
}
for (int i = 0; i < m; i++)
{
int j = randint(i, n - 1);
int t = x[i];
x[i] = x[j];
x[j] = t;
}
sort(x, x + m);
for (int i = 0; i < m; i++)
cout << x[i] << "\t";
cout << endl;
}
int main()
{
int m = 5, n = 10;
while (cin >> n >> m)
{
rand3(m, n);
}
system("pause");
return 0;
}
上述算法需要n个元素的存储空间,以及O(n+mlogm)的运行时间。
原理
本章示例了编程过程中的几个重要步骤,在实际应用的算法设计以及程序实现时,我们必须遵循以下原理:
- 正确理解所遇到的问题
- 提炼出抽象问题,简洁、明确的问题陈述不仅可以帮助我们解决当前遇到的问题,还有助于我们把解决方案应用到其他问题中;
- 考虑尽可能多的解法,非正式的高级语言可以帮助我们描述设计方案:伪代码表示控制流,抽象数据类型表示关键的数据结构。
- 实现一种解决方案
- 回顾