水塘抽样算法

简介

做用:水塘抽样算法是一种抽样算法,对于一个很大的集合,抽取的样本值可以保证随机.java

特色:其复杂度并不很高O(n),而且可以很大程度地节省内存.面试

问题导入

不少大公司的面试题都考察过这个算法,以谷歌为例,有一道关于水塘抽样的例题算法

我有一个长度为N的链表,N的值很是大,我不清楚N的确切值.我怎样能写一个尽量高效地算法来返回K个彻底随机的数.数组

这道题有两个限制:数据结构

1.高效,即节省内存的使用dom

2:尽可能随机地返回值函数

假如咱们去掉限制1,能够很简单地作出来,将全部数据加载进内存,计算链表长度,而后经过random函数来求取几个随机数.大数据

这样的效率并不高,把全部数据加载到内存,若是数据很是大可能会致使没法计算.spa

注意题目中有一个小tip,就是链表.链表这种数据结构是经过数据节点首尾相连造成的链式存储结构.code

既然是链表,那么能够一个一个节点处理,不须要将全部数据加载到内存.一个节点一个节点去处理,这还不够形象,将题目换个形式来表述:

咱们有1T的文本文件存在硬盘中,想随机抽取几行,保证尽量少得使用内存而且可以彻底随机.

以前想到的加载到内存就不太适合了,可是还能够想到别的办法,好比每次读取一行记录加载到内存,记数+1,清空内存中行数据,直到最后统计一共多少行,而后根据总行数来计算K个随机数.如何再取回行对应的数据呢?咱们能够再遍历一遍,一边遍历一边记录这一行的号码是否是在k个随机数中,若是是,则将该行内容保留.

这样的话遍历两次应该能够作到,可是1T数据遍历两次的时间消耗是很是高的.

因此还有更好的方案吗,那就是水塘抽样算法.

水塘抽样算法实现

具体例子

咱们先从具体案例中理解水塘抽样算法的实现,再从抽象的角度来理解.

假如10000个数,咱们要抽取十个随机数.

一万个数的样本集合数组记做S.

十个随机数的数组记做R,表明result.

先取数组S中前十个数填充进数组R.

算法的第一次迭代流程是这样的:

  • 从第十一个数(下标为10)开始迭代,生成一个0到10的随机整数j,若是j<10(假如J=4),咱们就将数组R中的第5项(R[4])替换成S数组中的第11项(S[10]).

遍历完成生成的R数组,就是咱们要求的随机数组.

抽象概念

$ S[N] $记做:样本集合

$ R[K] $记做:结果集合

$ N $记做:S数组大小

\(J\)记做:每次的随机数

\(K\)记做:前K个随机数

\(i\)记做:迭代次数.

步骤

  • \(S\)集合中前\(K\)个数填入\(R\)集合

  • \(S[K]\)开始遍历

    生成随机数\(J\),范围是\(0->K+i-1\).由于数组下标从0开始,因此-1.

    若是\(J<K\),则替换\(R\)中的值->\(R[j] = S[i]\).

  • 遍历结束,生成结果数组\(R\).

算法实现(JAVA)

int[] S = new int[10000];
        int N = S.length;
        Random random = new Random();
        //生成一万个数的数组
        for (int r = 0;r < N; r ++){
            S[r] = random.nextInt(10000);
        }
        
        int k = 10;
        int[] R = new int[k];
        //S前K个数填充R数组
        for (int f = 0;f < k; f++){
            R[f] = S[f];
        }
        int j ;
        //遍历数组S,根据算法,替换R数组中的元素,最终生成结果R数组.
        for (int i = k;i < S.length;i++){
            j = random.nextInt(i);
            if (j < k)  R[j] = S[i];
        }
        //打印R数组的结果
        for (int i =0;i < R.length;i++) {
            System.out.println(R[i]);
        }

总结一下这种算法.经过一遍遍历就得到了K个随机数,在很大数据的状况下效率是很是高的,很是适合咱们的应用场景.

可是为何这样生成的数是彻底随机的呢?

就刚才的具体例子来说,第一次遍历时,i=10,随机数的范围是0到10共11个数,那么不替换的几率是\(10/11\),等到第二次迭代时,不替换的几率变成\(10/12\),第三次\(10/13\),第四次\(10/14\).......

这样看来好像每一次的几率并不相等,其实并非这样,咱们要看的是最终进入数组\(R\)中的几率,虽然第十一个数进入\(R\)的几率比较大,可是到最后他被替换的几率也很大,因此每一个数最终保留在\(R\)中的几率究竟是多少呢?

能够参考一下维基百科中的证实,我以为很是清晰.

在循环内第n行被抽取的机率为k/n,以\(P_n\)表示。若是档案共有N行,任意第n行(注意这里n是序号,而不是总数).

被抽取的机率为:

咱们能够求得每行被抽取的几率是相同的,等于\(k/N\).

很是巧妙,因此当咱们面对这种情景时,能够考虑使用水塘抽样进行随机抽取.

相关文章
相关标签/搜索