1. 题目
读题
有几种算法可以实现从n个不同元素的数组中等概率地取出m个不同元素,
考查点
这个算法题考察的是如何从一个数组中等概率地随机抽取一定数量的元素,也就是洗牌算法或蓄水池抽样的应用。
有几种常见的实现方法,比如:
- Fisher-Yates Shuffle算法:每次从数组中随机选一个数,然后与数组尾部交换,直到选够m个数。2
- Knuth-Durstenfeld Shuffle算法:类似于Fisher-Yates Shuffle算法,但是从数组头部开始交换,效率更高。2
- Inside-Out算法:设置一个游标i从前向后扫描原始数据的拷贝,在 [0,i]之间随机一个下标j,然后用位置j的元素替换掉位置i的数字,再用原始数据位置i的元素替换掉拷贝数据位置j的元素。2
- 蓄水池抽样算法:当无法确定数组的大小时,可以使用这种方法。先将数组前m个数字存到结果数组中,然后从第m+1个数开始,以m/k的概率选择第k个数,并与结果数组中随机一个元素交换。23
2. 解法
思路
代码逻辑
具体实现
其中一种是Knuth-Durstenfeld Shuffle算法,它的思想是:
- 将1到n的数字存到数组中
- 从数组中取一个1到剩下数字个数的随机数k
- 从低位开始,将数组第k个数字取出,并保存到结果数组末尾
- 重复第2步,直到取出m个数字
这种算法的时间复杂度是O (n),空间复杂度是O (m)。
用Java语言实现的话,可以参考以下代码:
import java.util.Random;
public class RandomSelect {
// 从n个不同元素的数组中等概率地取出m个不同元素
public static int[] select(int[] array, int m) {
int n = array.length;
if (m > n) {
throw new IllegalArgumentException("m cannot be larger than n");
}
int[] result = new int[m]; // 存放结果的数组
Random random = new Random(); // 随机数生成器
for (int i = 0; i < m; i++) {
int k = random.nextInt(n - i) + i; // 生成一个i到n-1之间的随机数
result[i] = array[k]; // 将第k个元素放入结果数组
swap(array, i, k); // 将第k个元素与第i个元素交换,保证已选中的元素不会再被选中
}
return result;
}
// 交换数组中两个位置的元素
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 测试数组
int m = 4; // 要取出的元素个数
int[] result = select(array, m); // 调用算法
for (int num : result) { // 打印结果
System.out.print(num + " ");
}
}
}
3. 总结
这种算法有什么优缺点?
Knuth-Durstenfeld Shuffle算法的优点
- 是它的时间复杂度和空间复杂度都很低,只需要O (n)的时间和O (1)的空间,
- 而且能保证每个元素出现在每个位置上的概率相等。
它的缺点
- 是它是一个in-place算法,会改变原始数据的顺序,而有些场景中我们需要保留原始数据。
- 另外,它依赖于随机数生成器的质量,如果随机数生成器不够均匀或者有偏差,那么洗牌的结果也会受到影响。