抽样问题——《编程珠玑》读书笔记

        问题:输入两个整数m和n,并且m<n。输出一个由m个随机数字组成的有序列表,这些随机数的范围是[0, n-1],并且每个整数最多出现一次。

        方法一:

        Knuth著作《Seminumerical Algorithms》中提出的方法,顺序遍历n个数,通过随机测试条件的元素被选择。

        以一个例子来解释所说的随机测试条件,比如m=2,n=5。第一个元素0被选择的概率是2/5;第二个元素1被选择的概率取决于第一个元素有没有被选择,如果0被选择,则1被选择的概率为1/4,否则为2/4,所有1被选择的概率为(2/5)*(1/4)+(3/5)*(2/4)=2/5;同理第三个元素2被选择的概率取决于前两个的选择情况,如果都没被选择,则2被选择的概率为2/3,如果前两个有一个被选择,则2被选择的概率为1/3,如果前两个都被选择,则2被选择的概率为0,故2被选择的概率为(3/5)*(3/5)*(2/3)+2*(2/5)*(3/5)*(1/3)=2/5。依次类推,每个元素被选择的概率都为2/5。

        总的来说,从剩下的r个元素中选择s个元素,那么下一个元素被选中的概率为s/r,从整个数据集合角度来讲,每个元素被选择的概率都是相同的。

        这个思想的为代码如下:

select = m
remaining = n
for i = [0, n)
        if (bigrand() % remaining) < select
                print i
                --select
        --remaining

       首先,该算法可以保证有m个元素被选中,不会多也不会少。证明如下,首先证明不会多于m个:因为select等于0时不能选择更多的整数;再证明不会少于m个:当select/remaining=1时,总会选中一个元素,因为bigrand()%remaining<remaining总成立,所以i总会被选中。

       其次,每个元素被选择的概率是相等的,均为m/n,证明如上举例证明所示。       C++实现代码如下,同时算出从268435455约2.7亿int可以表示的最大整数除以8,本来准备拿int的最大整数约21.5亿测试,但方法三要先new出这么大的空间,超出程序可以分配的最大堆栈空间)个数中选出10万个整数,测试该方法所用的时间,便于与后面的方法进行性能比较。

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <limits>

using namespace std;

void genknuth(int m, int n)
{
	time_t t_start, t_end;
	t_start = time(NULL);
	
	for (int i = 0; i != n; ++i)
		if ((rand() % (n-i)) < m)
		{
			cout << i << " ";
			--m;
		}

	cout << endl;
	t_end = time(NULL);
	cout << "collapse time: " << difftime(t_end, t_start) << " s" << endl;
}

int main()
{
	int m = 100000;
	int n = numeric_limits<int>::max() / 8;
	srand(time(NULL));
	genknuth(m, n);
	cout << "n = " << n << endl;
	return 0;
}

       该算法的空间复杂度为O(m),时间复杂度为O(n)用这算法从2.7亿个数中随机找出10万个数所用时间为4秒

       方法二:

       方法一所需时间和搜索空间成正比,有些应用仍不能接受,因此需要继续改进。其中一种方法是随机插数据到一个容量为m的集合中。为代码如下所示:

initialize set S to empty
size = 0
while size < m do
        t = bigrand() % n
        if t is not in S
                insert t into S
                ++size
print the elements of S in sorted order

       C++代码实现如下所示,集合S的实现采用stl提供的set,底层用红黑树实现,不可重复插入相同的数据,当要插入的数据在set中已经存在时,则插入无效,数据不会被插入集合中,插入的时间复杂度为O(logm):

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <limits>
#include <set>

using namespace std;

void gensets(int m, int n)
{
	time_t t_start, t_end;
	t_start = time(NULL);

	set<int> S;
	while (S.size() < m)
		S.insert(rand() % n);
	for (set<int>::iterator iter = S.begin(); iter != S.end(); ++iter)
		cout << *iter << " ";

	cout << endl;
	t_end = time(NULL);
	cout << "collapse time: " << difftime(t_end, t_start) << " s" << endl;
}

int main()
{
	int m = 100000;
	int n = numeric_limits<int>::max() / 8;
	srand(time(NULL));
	gensets(m, n);
	return 0;
}

       算法的时间复杂度为O(mlogm),空间复杂度为O(m)。同样从2.7亿数据范围内选10万个数,所花的时间为2秒,可见速度会比原来的Knuth方法快。

       方法三:

        弄乱一个n个元素的数组,然后排序输出前m个元素。后来Ashley Shepherd和Alex Woronow发现,只需弄乱数组前m个元素,关于产生随机序列的方法可以参考我的文章《洗牌程序》维基百科

       本方法的C++代码实现如下:

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <limits>
#include <algorithm>

using namespace std;

// generate a random number between i and j,
// both i and j are include.
int randint(int i, int j)
{
	int ret = i + rand() % (j - i + 1);
	return ret;
}

void genshuf(int m, int n)
{
	time_t t_start, t_end;
	t_start = time(NULL);

	int i, j;
	int *x = new int[n];
	for (i = 0; i != n; ++i)
		x[i] = i;
	for (i = 0; i != m; ++i)
	{
		j = randint(i, n-1);
		int t = x[i]; x[i] = x[j]; x[j] = t; // swap x[i] and x[j]
	}
	sort(x, x + m);
	for (i = 0; i != m; ++i)
		cout << x[i] << " ";
	
	cout << endl;
	t_end = time(NULL);
	cout << "collapse time: " << difftime(t_end, t_start) << " s" << endl;

	delete []x;
	x = NULL;
}

int main()
{
	int m = 100000;
	int n = numeric_limits<int>::max() / 8;
	srand(time(NULL));
	genshuf(m, n);
	return 0;
}

        算法的时间复杂度是O(n+mlogm),空间复杂度是O(n)。同样从数据范围内选10万个数,所花时间为4秒,时间和方法一差不多,其中有一份部分时间花在初始化数组上,如果采用《编程珠玑》问题1.9的方法,当用到某个数时才初始化,这样算法的时间复杂度可以减少到O(mlogm)。不过空间复杂度O(n)还是太大。

        关于具体采用方法二还是方法三,stackflow上有个大牛用数学的方法证明了一下,当m<<n时,采用方法二会比三性能好。

参考文章:

http://www.cnblogs.com/2010Freeze/archive/2012/02/27/2370284.html

http://hi.baidu.com/23star/blog/item/47f7314e5c3b0e01b2de0574.html

Taking Random Samples

A Sample of Brilliance, Programming Perls




  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《编程珠玑 续》是Peter Norvig在原作《编程珠玑》基础上的延续,旨在进一步探讨编程技巧和优化方法。本书通过大量实例和案例,帮助读者更好地理解和运用编程的精髓。 本书主要涵盖了以下几个方面: 首先,作者介绍了一些高效的算法和数据结构,以帮助读者更好地解决各类实际问题。例如,作者详细讲解了常用的排序算法和搜索算法,在实际应用中如何选择最合适的算法进行优化。 其次,本书还涉及了一些高级的编程技巧和思维模式。作者以实际案例为依据,深入讲解了如何进行代码重构、如何处理复杂的数据结构、如何进行并行计算等等。这些技巧和模式可以使读者的代码更加简洁、高效和可维护。 此外,本书还对一些热门的编程语言和框架进行了介绍和比较。作者通过对比分析,帮助读者选择最适合自己项目需求的编程语言和框架,并介绍了它们的一些优缺点和使用技巧。 最后,作者还分享了一些自己的编程心得和经验,并对未来的发展趋势进行了预测。他鼓励读者积极参与开源项目,不断学习和提升自己的编程水平。 总而言之,《编程珠玑 续》是一本帮助读者深入理解编程精髓和优化技巧的实用指南。它丰富了原作的内容,并引入了新的案例和技巧,对于专业程序员和对编程感兴趣的人都是一本值得阅读的书籍。通过学习本书,读者能够更好地提升自己的编程能力,解决实际问题,并更好地适应行业的不断变化和发展。 ### 回答2: 《编程珠玑 续》是一本继承《编程珠玑》精神的编程类图书,它深入探讨了更多关于编程算法的话题,帮助读者进一步提升编程技巧和解决问题的能力。 这本续集书籍首先延续了原版的思维方式和编程风格,鼓励读者通过实践和思考来掌握编程的本质。它从不同的角度和实际场景出发,提供了更多实用的编程技巧和解决问题的方法,使读者能够更加高效地编写代码。 《编程珠玑 续》的内容涵盖了多个领域,包括排序算法、字符串处理、数据结构、网络编程等。它介绍了一些经典的算法和数据结构,并通过大量的例子和实践题目帮助读者加深理解和掌握。 此外,《编程珠玑 续》还关注了一些系统设计和性能优化的问题,提供了一些实际应用的案例和经验分享。通过学习这些内容,读者可以更好地设计和构建可扩展、高性能的软件系统。 总的来说,《编程珠玑 续》是一本非常实用的编程类书籍,它以深入浅出的方式讲解了多个编程算法的关键概念,帮助读者在解决问题和编写代码时更具洞察力和技巧。无论是编程初学者还是有一定经验的开发工程师,都可以从中获得很多启发和收获。 ### 回答3: 《编程珠玑续》是由Jon Bentley所著的计算机编程经典著作《编程珠玑》的续篇。在这本续作中,作者进一步探讨了计算机科学和编程的一些重要问题和技巧。 《编程珠玑续》以问题为中心,通过讲解不同的编程问题和解决方案,培养读者的编程思维和解决问题的能力。书中的问题涉及各个领域,包括算法设计、数据结构、性能优化、并发编程等内容,内容丰富而实用。 这本书的编写风格类似于《编程珠玑》,采用了一种琐碎而有趣的方式来讲解问题,引导读者逐步分析和优化解决方案。通过这种方法,读者可以深入了解各种编程技巧和策略,从而提高自己的编程水平。 《编程珠玑续》还包含了许多实际案例和代码示例,读者可以通过实践来巩固所学的知识。这使得书籍的内容更加贴近实际编程应用,并能帮助读者更好地理解和运用所学的技巧。 总之,如果你对计算机科学和编程有浓厚的兴趣,并希望深入了解和掌握一些编程问题和技巧,那么《编程珠玑续》是一本非常值得阅读的书籍。它将帮助你提升编程能力,并成为你在实际编程中的得力助手。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值