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

2023-05-16

项目github地址:bitcarmanlee easy-algorithm-interview-and-practice
欢迎大家star,留言,一起学习进步

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());
        }
    }

上面的代码是在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

一共需要抽样出来10000*3=30000个数,每个数出现的次数平均为30000/24=1250次。上面的结果大致满足等概率均匀分布。

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

那么生成第x+1个数需要调用random方法的次数为:
E ( x + 1 ) = ( 1 − x n ) ∗ 1 + ( 1 − x n ) x n ∗ 2 + ⋯ + ( 1 − x n ) ( x n ) k − 1 ∗ k + ⋯ = n n − 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)=(1nx)1+(1nx)nx2++(1nx)(nx)k1k+=nxn
上述等差-等比数列求和的方法,见参考文献1,只需要中学数学知识即可理解。

则调用random方法的总次数期望为:
E ( r a n d o m ) = n n + n n − 1 + ⋯ + n n − m − 1 ≈ O ( n ( l g ( n ) − l g ( 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)=nn+n1n++nm1nO(n(lg(n)lg(nm)))
当m接近n时,此时时间复杂度接近 O ( n l o g n ) 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;
    }

其中,n是原始样本长度,m为待抽取样本个数。

2.时间复杂度为O(n)的从N个数中抽取M个的算法

上面的算法,时间复杂度为 O ( n l g n ) 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());
        }
    }

上面代码模拟的是从{0, 1, 2, 3, 4}中随机抽取两个数,重复10000次。
最后运行的结果如下:

0: 4056
1: 4001
2: 3903
3: 4102
4: 3938

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\n",i);
			t--;
		}
}

上面的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.需要保证每个样本被抽到的概率是 m n \frac{m}{n} nm
2.第一个样本按 m n \frac{m}{n} nm的概率进行抽样即可。
3.对于第二个样本,如果第一个样本被抽中,其被抽中的概率为 m − 1 n − 1 \frac{m-1}{n-1} n1m1。如果第一个样本没有被抽中,其被抽中的概率为 m n − 1 \frac{m}{n-1} n1m。第二个样本被抽中的概率为 m − 1 n − 1 ∗ m n + m n − 1 ∗ ( 1 − m n ) = m n \frac{m-1}{n-1} * \frac{m}{n} + \frac{m}{n-1} * (1 - \frac{m}{n}) = \frac{m}{n} n1m1nm+n1m(1nm)=nm
4.对于第i个样本,被抽中的概率为 m − k n − i + 1 \frac{m - k}{n - i + 1} ni+1mk,其中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());
        }
    }

最后的输出结果为:

0: 3013
1: 2941
2: 2929
3: 3060
4: 3020
5: 3047
6: 3058
7: 3067
8: 2917
9: 2948

参考文献:
1.https://zh.wikipedia.org/wiki/%E7%AD%89%E5%B7%AE-%E7%AD%89%E6%AF%94%E6%95%B0%E5%88%97
2.https://blog.csdn.net/bitcarmanlee/article/details/52719202

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

最全随机抽样算法(从N个数中抽取M个等)集合 的相关文章

随机推荐

  • 【MEMS传感器】ICM20690六轴传感器SPI驱动

    此驱动适合ICM206xx系列 xff0c 基本上更改who am i寄存器和一些特殊功能寄存器就可以 xff0c 这里以获取角速度与陀螺为例 初始化调用两个函数 xff1a ICM20690 CHIP INIT ICM20690 CHIP
  • 每次在boot下建立ssh空文件就会被删掉,wpa_supplicant.conf 文件也会自动被删除,为什么?

    A 开启ssh 和 配置WiFi 注意2 xff1a 我知道你可能没有路由器 xff0c 你也可以用电脑开启WiFi热点 xff0c 然后在热点管理那里找到你树莓派的IP地址 如果你没有给树莓派现在就连接屏幕的想法 xff0c 第一次启动O
  • Jetson查看JetPack版本(tool)

    Jetson查看JetPack版本 查看L4T版本 我的L4T版本为 32 5 1 在官网查找对应的jetpack版本 This page includes access to previously released versions of
  • ros中设置Global Options,以及rqt_tf_tree树讲解,TF树的理解,使用GUI插件,用于可视化ROS-TF的框架树

    一 设置Global Options 如图30 启动rviz界面后 首先要对Global Options进行设置 Global Options里面的参数是一些全局显示相关的参数 其中的Fixed Frame参数是全局显示区域依托的坐标系 我
  • jetson xavier和NX 以及nano如何通过sd卡启动,sdkManager刷机

    1 xff0c xavier可以通过迁移系统的方法进行 Jetson AGX Xavier从SD卡启动系统及系统移植 xavier AGX不能直接使用sdkManager刷机到SD卡 xff0c 所以需要用系统迁移的方法 2 xff0c N
  • Linux NameSpace (目录)

    1 User Namespace 详解 2 Pid Namespace 详解 3 Mnt Namespace 详解 4 UTS Namespace 详解 5 IPC Namespace 详解 6 Network Namespace 详解
  • Python Nose 自动化测试框架介绍

    文章目录 1 unittest 简介1 1 python 单元测试1 2 unittest 测试框架1 3 默认模式1 4 手工模式 2 nose 扩展框架2 1 96 nose 96 的安装和基本用法2 2 96 被测对象 96 的扩展2
  • TLSF 内存分配算法详解

    文章目录 1 DSA 背景介绍1 1 mmheap1 2 mmblk 2 TLSF 原理2 1 存储结构2 2 内存池初始化2 3 free2 4 malloc 参考资料 1 DSA 背景介绍 动态内存管理算法 DSA xff0c Dyna
  • 史上最全采样方法详细解读与代码实现

    项目github地址 xff1a bitcarmanlee easy algorithm interview and practice 欢迎大家star xff0c 留言 xff0c 一起学习进步 1 什么是采样 在信号系统 数字信号处理中
  • Linux Kdump 机制详解

    文章目录 1 简介1 1 安装1 2 触发 kdump1 3 调试 kdump1 3 1 安装 debuginfo vmlinux1 3 2 编译 kernel 1 4 kdump tools service 流程分析 2 原理分析2 1
  • Buildroot 用户手册 (中文)

    文章目录 I Getting started1 About Buildroot2 System requirements2 1 Mandatory packages2 2 Optional packages 3 Getting Buildr
  • 正则表达式 (学习笔记)

    正则表达式的难度不在于难懂 xff0c 而在于对它的表述没有恰当的分类和组织 xff0c 所以弄得很零散难以记忆 按照自己的理解和归纳记录一份笔记 xff0c 以备遗忘时查看 正则表达式 regular expressions 是一种用来匹
  • Linux usb 2. 协议分析

    文章目录 0 背景1 USB 协议传输格式1 1 Packet1 1 1 Token Packet1 1 2 Data Packet1 1 3 Handshake Packet1 1 4 Special Packet 1 2 Transac
  • RISCV 入门 (学习笔记)

    文章目录 1 risv 相关背景1 1 arm 授权费1 2 riscv 发展历史1 3 riscv 风险 2 指令集2 1 可配置的通用寄存器组2 2 规整的指令编码2 3 简洁的存储器访问指令2 4 高效的分支跳转指令2 5 简洁的子程
  • Linux usb 1. 总线简介

    文章目录 1 USB 发展历史1 1 USB 1 0 2 01 2 USB 3 01 3 速度识别1 4 OTG1 5 phy 总线1 6 传输编码方式 2 总线拓扑2 1 Device 内部的逻辑关系2 2 Compound Compos
  • Linux usb 3. Host 详解

    文章目录 1 简介2 Usb Core 驱动设备模型2 1 Usb Device Layer2 1 1 device struct usb device 2 1 2 driver struct usb device driver 2 1 3
  • Linux usb 4. Device 详解

    文章目录 1 简介2 Platform Layer2 1 Platform Device2 2 Platform Driver 3 UDC Gadget Layer3 1 Gadget Bus3 2 Gadget Device3 2 1 E
  • Linux USB (目录)

    1 USB 总线简介 2 USB 协议分析 3 USB Host 详解 4 USB Device 详解 5 usbip USB Over IP 使用实例 6 USB HC UDC 测试 7 Linux 配置 ADBD
  • Linux usb 5. usbip (USB Over IP) 使用实例

    文章目录 0 简介1 Server 配置2 Client 配置参考资料 0 简介 USB Over IP 是一种应用很多的场景 xff0c 目前已经有现成的解决方案 usbip linux 和 windows 环境下都有配套软件 xff0c
  • 最全随机抽样算法(从N个数中抽取M个等)集合

    项目github地址 xff1a bitcarmanlee easy algorithm interview and practice 欢迎大家star xff0c 留言 xff0c 一起学习进步 1 从N个数中等概率抽取M个数 从N个样本