30 从n个数中随机获取m个数字

时间:2023-01-08 17:44:31

前言

本博文部分图片, 思路来自于剑指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 970655147
*/


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, 所以请大家指出!