《编程珠玑》阅读小记(9) — 取样问题

问题

本章研究的问题是取样问题,也就是程序设计中的随机数,问题描述如下:
程序的输入包含两个整数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)的运行时间。

原理

本章示例了编程过程中的几个重要步骤,在实际应用的算法设计以及程序实现时,我们必须遵循以下原理:

  • 正确理解所遇到的问题
  • 提炼出抽象问题,简洁、明确的问题陈述不仅可以帮助我们解决当前遇到的问题,还有助于我们把解决方案应用到其他问题中;
  • 考虑尽可能多的解法,非正式的高级语言可以帮助我们描述设计方案:伪代码表示控制流,抽象数据类型表示关键的数据结构。
  • 实现一种解决方案
  • 回顾

你可能感兴趣的:(编程)