• 最全随机抽样算法(从N个数中抽取M个等)集合


    1.从N个数中等概率抽取M个数
    从N个样本中等概率抽取M个样本(M<N)是常见的需求。现在我们以一个数组来模拟样本,看看怎么实现这个算法。
    最容易想到的方法,肯定就是直接等概率抽取。具体做法如下:每次都随机在[0, N-1](假设第一个样本d的标号为0)之间抽取一个数,并且与之前的数相比较。如果与前面生成的随机数相同,则继续随机生成,直到生成一个与之前所有生成数不同的数。如果不相同,则将该随机数添加到结果集中,并继续随机抽取,直至结果集中的数为M个。

        public static Set<Integer> sampletest() {
            Set<Integer> set = new HashSet<>();
            int first = RandomUtils.nextInt(0, 24);
            int second = RandomUtils.nextInt(0, 24);
            int third = RandomUtils.nextInt(0, 24);
            set.add(first);
            while(set.contains(second)) {
                second = RandomUtils.nextInt(0, 24);
            }
            set.add(second);
            while(set.contains(third)) {
                third = RandomUtils.nextInt(0, 24);
            }
            set.add(third);
            return set;
        }
        public static void samplemassive() {
            Map<Integer, Integer> map = new HashMap();
            for(int i=0; i<10000; i++) {
                Set<Integer> res = sampletest();
                for(int each: res) {
                    map.put(each, map.getOrDefault(each, 0) + 1);
                }
            }
            for(Map.Entry<Integer, Integer> entry: map.entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    上面的代码是在24个数中随机抽取3个数,然后将该抽样重复一万次,输出最后的结果。
    将samplemassive方法run起来以后,输出结果如下:

    0: 1218
    1: 1239
    2: 1195
    3: 1200
    4: 1213
    5: 1282
    6: 1297
    7: 1241
    8: 1200
    9: 1270
    10: 1272
    11: 1277
    12: 1250
    13: 1270
    14: 1233
    15: 1212
    16: 1298
    17: 1228
    18: 1238
    19: 1212
    20: 1209
    21: 1308
    22: 1308
    23: 1256
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    一共需要抽样出来10000*3=30000个数,每个数出现的次数平均为30000/24=1250次。上面的结果大致满足等概率均匀分布。

    上面算法的问题在于,当m比较大的时候,每次调用random方法生成的数与之前重合的概率也会越来越大,则while循环里random的调用次数会越来越多,这样时间复杂度就会升高。
    那么具体的时间复杂度是多少呢?可以定量分析一下。
    假设之前已经生成了x个数,接下来生成第x-1个数。
    第一次调用random就成功生成第x-1个数的概率为1−xn 1 - frac{x}{n}1− 
    n
    x
    ​    
     
    第二次调用random就成功生成第x-1个数的概率为(1−xn)xn (1 - frac{x}{n})frac{x}{n}(1− 
    n
    x
    ​    
     ) 
    n
    x
    ​    
     
    第k次调用random就成功生成第x-1个数的概率为(1−xn)(xn)k−1 (1 - frac{x}{n}){(frac{x}{n})}^{k-1}(1− 
    n
    x
    ​    
     )( 
    n
    x
    ​    
     ) 
    k−1
     

    那么生成第x+1个数需要调用random方法的次数为:
    E(x+1)=(1−xn)∗1+(1−xn)xn∗2+⋯+(1−xn)(xn)k−1∗k+⋯=nn−x E(x+1) = (1 - frac{x}{n}) * 1 + (1 - frac{x}{n})frac{x}{n} * 2 + cdots + (1 - frac{x}{n}){(frac{x}{n})}^{k-1} * k + cdots = frac{n}{n-x}
    E(x+1)=(1− 
    n
    x
    ​    
     )∗1+(1− 
    n
    x
    ​    
     ) 
    n
    x
    ​    
     ∗2+⋯+(1− 
    n
    x
    ​    
     )( 
    n
    x
    ​    
     ) 
    k−1
     ∗k+⋯= 
    n−x
    n
    ​    
     

    上述等差-等比数列求和的方法,见参考文献1,只需要中学数学知识即可理解。

    则调用random方法的总次数期望为:
    E(random)=nn+nn−1+⋯+nn−m−1≈O(n(lg(n)−lg(n−m))) E(random) = frac{n}{n} + frac{n}{n-1} + cdots + frac{n}{n-m-1} approx O(n(lg(n) - lg(n-m)))
    E(random)= 
    n
    n
    ​    
     + 
    n−1
    n
    ​    
     +⋯+ 
    n−m−1
    n
    ​    
     ≈O(n(lg(n)−lg(n−m)))

    当m接近n时,此时时间复杂度接近O(nlogn) O(nlogn)O(nlogn),算法的复杂度比较高。

    上面的sample算法比较笨,实现一个通用的从N个数抽取M个的算法。

        public static Set<Integer> sampletest(int n, int m) {
            Set<Integer> set = new HashSet<>();
            int first = RandomUtils.nextInt(0, n);
            set.add(first);
            while(set.size() < m) {
                int tmp = RandomUtils.nextInt(0, n);
                while(set.contains(tmp)) {
                    tmp = RandomUtils.nextInt(0, n);
                }
                set.add(tmp);
            }
            return set;
        }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    其中,n是原始样本长度,m为待抽取样本个数。

    2.时间复杂度为O(n)的从N个数中抽取M个的算法
    上面的算法,时间复杂度为O(nlgn) O(nlgn)O(nlgn)。那么有没有时间复杂度更低的算法呢?
    答案是有的,用蓄水池算法就可以实现。
    关于蓄水池算法的具体原理,可查阅参考文献2。
    直接上一个例子。

           public static int[] reservoir(int[] array, int m) {
            int[] result = new int[m];
            int n = array.length;
            for(int i=0; i<n; i++) {
                int current_num = array[i];
                if(i < m) {
                    result[i] = current_num;
                } else {
                    int tmp = RandomUtils.nextInt(0, i+1);
                    if(tmp < m) {
                        result[tmp] = current_num;
                    }
                }
            }
            return result;
        }

        public static void massive_reservoir() {
            int[] array = {0, 1, 2, 3, 4};
            int m = 2;
            Map<Integer, Integer> map = new HashMap();
            for(int i=0; i<10000; i++) {
                int[] result = reservoir(array, m);
                for(int each: result) {
                    map.put(each, map.getOrDefault(each, 0) + 1);
                }
            }
            for(Map.Entry<Integer, Integer> entry: map.entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    上面代码模拟的是从{0, 1, 2, 3, 4}中随机抽取两个数,重复10000次。
    最后运行的结果如下:

    0: 4056
    1: 4001
    2: 3903
    3: 4102
    4: 3938
    1
    2
    3
    4
    5
    3.随机抽取有序列表
    上面抽样的结果都是无序的,只需要满足最后出现的概率相等即可。例如从{0, 1, 2, 3, 4}中抽取两个数,有可能先抽到0,也有可能先抽到4。如果我们要求抽样结果是有序的,那该怎么办?
    这种情况在实际中很常见。比如在流量分配系统中,流量都是流式过来的,或者说是有序的。假设有十个流量依次过来,需要在这十个流量随机选择三个投放三个广告,并且每个流量投放广告的概率都相等。这种场景就跟抽取有序列表类似。
    在Knuth的《计算机程序设计艺术 第2卷 半数值算法》一书中,给出了一个算法。

    void GenerateKnuth(int n,int m)
    {
        int t=m;
        for(int i=0;i<n;i++)
            if(Rand(0,n-1-i)<t)//即以t/(n-i)的概率执行下面的语句
            {
                printf("%d ",i);
                t--;
            }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    上面的n是指待抽取的列表总长度,m为想要抽取的结果个数。

        public static List<Integer> randomtest() {
            int m = 3;
            int tmp = m;
            int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
            int len = array.length;
            List<Integer> list = new ArrayList<>();
            for(int i=0; i<len; i++) {
                if(RandomUtils.nextInt(0, len - i) < tmp) {
                    list.add(array[i]);
                    tmp--;
                }
            }
            return list;
        }

        public static void massive_randomtest() {
            Map<Integer, Integer> map = new HashMap();
            for(int i=0; i<10000; i++) {
                List<Integer> list = randomtest();
                for(int each: list) {
                    map.put(each, map.getOrDefault(each, 0) + 1);
                }
            }
            for(Map.Entry<Integer, Integer> entry: map.entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    上面代码的写法是按照算法的思路来的。我在项目实现过程中,想了另外一种更容易理解,也更只管的实现方式。可以进行简单的证明如下:
    1.需要保证每个样本被抽到的概率是mn frac{m}{n} 
    n
    m
    ​    
     
    2.第一个样本按mn frac{m}{n} 
    n
    m
    ​    
     的概率进行抽样即可。
    3.对于第二个样本,如果第一个样本被抽中,其被抽中的概率为m−1n−1 frac{m-1}{n-1} 
    n−1
    m−1
    ​    
     。如果第一个样本没有被抽中,其被抽中的概率为mn−1 frac{m}{n-1} 
    n−1
    m
    ​    
     。第二个样本被抽中的概率为m−1n−1∗mn+mn−1∗(1−mn)=mn frac{m-1}{n-1} * frac{m}{n} + frac{m}{n-1} * (1 - frac{m}{n}) = frac{m}{n} 
    n−1
    m−1
    ​    
     ∗ 
    n
    m
    ​    
     + 
    n−1
    m
    ​    
     ∗(1− 
    n
    m
    ​    
     )= 
    n
    m
    ​    
     。
    4.对于第i个样本,被抽中的概率为m−kn−i+1 frac{m - k}{n - i + 1} 
    n−i+1
    m−k
    ​    
     ,其中k为前面已经抽中的个数,k<=m。即抽取第i个样本时候,如果前面已经抽中了k个,那么需要在剩下的n-i+1个样本中抽取m-k个。

    按照我自己理解的思路再实现一下,代码更简单,思路也更清晰一些:

        public static List<Integer> randomtest() {
            int m = 3;
            double costednum = 0.0;
            int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
            int len = array.length;
            List<Integer> list = new ArrayList<>();
            for(int i=0; i<len; i++) {
                double probability = (m - costednum) / (len - i);
                double value = Math.random();
                if(probability > value) {
                    list.add(array[i]);
                    costednum += 1;
                }
            }
            return list;
        }

        public static void massive_randomtest() {
            Map<Integer, Integer> map = new HashMap();
            for(int i=0; i<10000; i++) {
                List<Integer> list = randomtest();
                for(int each: list) {
                    map.put(each, map.getOrDefault(each, 0) + 1);
                }
            }
            for(Map.Entry<Integer, Integer> entry: map.entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    最后的输出结果为:

    0: 3013
    1: 2941
    2: 2929
    3: 3060
    4: 3020
    5: 3047
    6: 3058
    7: 3067
    8: 2917
    9: 2948
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    参考文献:
    1.https://zh.wikipedia.org/wiki/等差-等比数列
    2.https://blog.csdn.net/bitcarmanlee/article/details/52719202
    --------------------- 
    作者:bitcarmanlee 
    来源:CSDN 
    原文:https://blog.csdn.net/bitcarmanlee/article/details/83016377 
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    flask——flask-sqlachemy中的一对多,多对多关系
    flask——flask-sqlachemy的使用,模块划分
    Ubuntu下安装JDK和Open Jdk
    flask——全文检索
    Flask——部署
    在Ubuntu上安装Chrome浏览器和ChromeDriver
    jinja2模板语言的循环序号
    CentOS7安装mongodb
    CentOS7安装PhantomJS
    使用screen在后台运行程序
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13313651.html
Copyright © 2020-2023  润新知