《编程珠玑》阅读小记(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)的运行时间。

原理

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

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

《编程珠玑》阅读小记(9) — 取样问题的相关文章

编程珠玑阅读笔记01

薄薄的一本书,丝毫无愧于珠玑两个字. 看了第一章,我对这本书佩服得五体投地.一个简洁的小例子,几个看似简单的算法,实际上包含了很多算法设计的思想.看完第一章,我对数据库的几种外排算法有了更深层次的理解 习题:位图和位向量来表示集合 例如集合{1,2,3,5,8,13}可表示为:`0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0` (从左到右为第0~19位) 文中巧妙的对整数集合排序的思路分三步:  1. 数组所有位初始为0 2. 将输入的整数n当作index来用,出

《编程珠玑》阅读小记(1)— 开篇

1. 前言 久闻<编程珠玑>一书的大名,一直没有找到合适的机会深入学习阅读,最近终于得以入手,便决心投入细细的研究,提升一下自己的编程思想与技术.阅读之后才发现,这本书确实一本不可多得的好书.它以计算机领域应用与编程算法相结合,让读者面对实际问题时,不单单局限于考虑该问题的解决方案,而是在入手实践之前能够驻足于考虑,该方案是否符合当前的实际环境,它的时间与空间的消耗是否达到了一个比较好的指标. 通过阅读这本书,很大程度上拓宽了我这样一个菜鸟程序员的视野.对于我来讲,发现要想真正的对书中内容有所

《编程珠玑》阅读小记(11) — 堆

章节简述 本章主要介绍堆,用该数据结构解决下面两个重要的问题: 排序,采用堆排序算法对n元数组排序,所花的时间不会超过O(nlogn),而且只需要几个字的额外空间: 优先级队列,堆通过插入新元素和提取最小元素这两种操作来维护元素集合,每个操作所需的时间都为O(logn): 本章采用自底向上的组织结构,从细节开始逐步过渡到正题. 堆数据结构 该部分介绍堆数据结构的设计思想. 优先级队列实现向量排序算法 优先级队列提供了一种简单的向量排序算法,优先在优先级队列中依次插入每个元素,然后按序删除它们,程

《编程珠玑》阅读小记(4) — 编写正确的程序

本章简述 本章的主题是编写正确的程序,以一个二分搜索算法引入. 关于二分搜索 二分搜索的关键思想是如果t在x[0..n-1]中,那么它就一定存在于x的某个特定范围之内.该程序最重要的部分是大括号内的循环不变式,也就是关于程序状态的断言. 代码的开发是自上而下进行的(从一般思想开始,将其完善为独立的代码行),该正确性分析则是自下而上进行的,从每个独立的代码行开始,检查它们是如何协同运作并解决问题的. 关于循环是程序中比较重要的部分,关于其正确性的讨论分为3个部分,每个部分都与循环不变式密切相关.

《编程珠玑》阅读小记(6) — 算法设计技术

本章简述 通过前面第二章节的叙述,描述了算法设计对程序员的日常影响:算法上的灵机一动可以使程序更加简单.但是本章内容将会发现算法设计的一个不那么常见但更富于戏剧性的贡献:复杂深奥的算法有时可以极大地提高程序性能. 问题及简单算法 本章引入的问题来自一维的模式识别,问题的输入是具有n个浮点数的向量x,输出是输入向量的任何连续子向量中的最大和. 例如,如果输入向量包含以下 N = 10 个元素: arr[N] = { 31, -41, 59, 26, -53, 58, 97, -93, -23, 8

《编程珠玑》高清pdf版

下载地址:网盘下载 作者简介 编辑 Jon Bentley是位于新泽西州Murray Hill的朗讯贝尔实验室计算机科学研究中心的技术委员会委员,Jon自1998年就成为Dr. Dobb's Joumal杂志的特约编辑,他的"编程珠玑"专栏多年来一直是顶级学术杂志Communications of the ACM最风行的特色专栏之一,而本书正是建立在这些专栏的基础之上. 目录 编辑 第一部分 基础 第1章 开篇 1.1 一次友好的对话 1.2 准确的问题描述 1.3 程序设计 1.4

编程珠玑 第2版 pdf

下载地址:网盘下载 内容简介  · · · · · · 本书是计算机科学方面的经典名著.书的内容围绕程序设计人员面对的一系列实际问题展开.作者Jon Bentley 以其独有的洞察力和创造力,引导读者理解这些问题并学会解决方法,而这些正是程序员实际编程生涯中至关重要的.本书的特色是通过一些精心设计的有趣而又颇具指导意义的程序,对实用程序设计技巧及基本设计原则进行了透彻而睿智的描述,为复杂的编程问题提供了清晰而完备的解决思路.本书对各个层次的程序员都具有很高的阅读价值.. 多年以来,当程序员们推选

编程珠玑高清pdf高清版免费下载

下载地址:网盘下载 备用地址:网盘下载 作者简介编辑Jon Bentley是位于新泽西州Murray Hill的朗讯贝尔实验室计算机科学研究中心的技术委员会委员,Jon自1998年就成为Dr. Dobb's Joumal杂志的特约编辑,他的“编程珠玑”专栏多年来一直是顶级学术杂志Communications of the ACM最风行的特色专栏之一,而本书正是建立在这些专栏的基础之上.目录编辑第一部分 基础 第1章 开篇 1.1 一次友好的对话 1.2 准确的问题描述 1.3 程序设计 1.4

【编程珠玑】【第一章】生成随机数、随机取样的问题

一.利用随机数函数生成随机数 问题1(<编程珠玑>习题12.1后半段): 给定一个rand(),可以产生从0到RAND_MAX的随机数,其中RAND_MAX很大(常见值:16位int能表示的最大整数32767),写出利用rand()生成[a,b]中任意整数的函数,其中a>=0, b<=RAND_MAX,且b-a<<RAND_MAX. 分析: 这是在编程工作最常见的随机函数的应用,在这里做一个起点再合适不过.把随机数区间的起点从0变为a,同时把一共RAND_MAX+1个数