前言

本博文部分图片, 思路来自于剑指offer 或者编程珠玑

问题描述

从给定的n个数中随机抽取m个数字
不知道 这道题目属不属于”编程珠玑”中的, 我找了一下 似乎没有找到

思路

思路一 : 总共需要找到m个数字, 找第一个数字 : 生成一个[0, n)的随机数k 获取该位置的数据; 找第二个数字 : 生成一个[0, n-1)的随机数k2, 那么 这时候就应该排除第一次找到的k对不对, so 如果k2>=k, 则令k2++, 这样就保证了在[0, k-1], [k+1, n)两个区间之间找到另外的一个随机数值
其他的数字 依次类推

思路二 : 从第1个元素 遍历到第n个元素, 找到当前元素的概率为 : (还剩下需要找的元素的个数 / 剩下的元素总数)
获取到第一个元素的概率为 : m / n
获取到第二个元素的概率为 : ( (m-1)/(n-1) * (m/n) ) + (m/(n-1) * (n-m/n) ) = (m^2-m + mn-m^2) / (n * (n-1) ) = m / n
其他的元素 依次类推

思路三 : 思路三可以说是从实现上来说 是最简单的了, shuffle一下给定的序列, 然后 获取前m位[规则 可自定义, 因为现在随机化了嘛]即可

参考代码

/**
 * file name : Test10SelectMFromN.java
 * created at : 11:11:40 AM Jun 22, 2015
 * created by 
 */

package com.hx.test06;

public class Test10SelectMFromN {

    // 随机的从n个数中选择m个数
    public static void main(String []args) {

        int range = 200;
        int n = 10, m = 4;
//      int[] arr = Tools.newIntArray(range, n);
        int[] arr = new int[]{0,  1,  2,  3,  4,  5,  6,  7,  8, 9 };
        Log.log(arr);
        long start = System.currentTimeMillis();

        selectMFromN01(arr, n, m);
        selectMFromN02(arr, n, m);
        selectMFromN03(arr, n, m);

        long spent = System.currentTimeMillis() - start;
        Log.log("spent : " + spent + " ms ...");
    }

    // random
    static Random ran = new Random();

    // 随机的从n个数中选择m个数
    // sleectedIdx 中包含的是已经选择了的数据的索引
    // selected 中包含的是选取的数据的索引
    // 思路 : 循环m次, 每次ran生成一个arr.length - i, 的数据的随机索引
        // 然后, 开始和已经选择了的数的索引进行比较, 如果这个随机数大于等于val 则selected++ [表示该位置的数据已经被选择了, 类似于第一次是从n个元素中找出一个k, 第二次是从n-1个元素中找出一个k2, 如果k2大于等于k, 则将k2的索引向后移动一位, 确保能够将k2移动到正确的位置]
            // 直到selectedIdx中某一个数据小于selected
        // 最后的 select即为当前循环所选取的索引
    // selecteIdx是用TreeSet维护的一个有序的集合[必需确保其有序]
    // 此方法 适合于m比较小的情况
    // 如果m太大的情况下, 可以转换一种思路, 选取(n-m)个数  作为排除的数
    public static void selectMFromN01(int[] arr, int n, int m) {
        Set<Integer> selectedIdx = new TreeSet<>();
        for(int i=0; i<m; i++) {
            int select = ran.nextInt(n - i);

            Iterator<Integer> it = selectedIdx.iterator();
            while(it.hasNext() ) {
                int val = it.next();
                if(select >= val) {
                    select ++;
                } else {
                    break ;
                }
            }

            selectedIdx.add(select);
        }

        for(Integer idx : selectedIdx) {
            Log.logWithoutLn(arr[idx] + " ");
        }
        Log.enter();
    }

    // 思路 : 从n-i 中获取一个随机值, 如果该值小于m, 则当前索引对应的数据加入到selected中, 然后  更新m
    // m减到了0, 则说明获取到的数据  已经够了, 后面则获取不到数据了, 因为(0, n-i)中没有比0小的
    // 获取第一个数据的概率为 m / n
        // 如果获取到第一个数据   则获取到第二个数据的概率为 m / (n-1)
            // 否则   获取第二个数据的概率为 m / (n-1)
        // 其他数据 一次类推..
    public static void selectMFromN02(int[] arr, int n, int m) {
        List<Integer> selected = new ArrayList<>(m);
        for(int i=0; i<n; i++) {
            int select = ran.nextInt(n - i);
            if(select < m) {
                selected.add(arr[i]);
                m --;
            }
        }

        Log.log(selected );
    }

    // 思路 : shuffle 前m个元素   然后 前m个元素即为所求
    // 此方法效率比较高
    public static void selectMFromN03(int[] arr, int n, int m) {
        List<Integer> selected = new ArrayList<>(m);
        for(int i=0; i<m; i++) {
            Tools.swap(arr, i, ran.nextInt(arr.length));
        }

        for(int i=0; i<m; i++) {
            selected.add(arr[i]);
        }

        Log.log(selected);
    }

}

效果截图

30 从n个数中随机获取m个数字_数据结构

总结

没想到思路二居然是正确的吧 ?
确实 这种看起来有时候感觉挺玄乎的东西, 确实是可以通过证明来证实其是正确的

思路三, 相比于思路一来说 则是大道至简的,
思路一 千幸万苦向随机找到剩余的备选数据中的一个, 然而 思路三, 却不用理会, 因为shuffle之后, 你根本不知道 “谁是谁”

注 : 因为作者的水平有限,必然可能出现一些bug, 所以请大家指出!