面试谈的算法
1. 问题模型描述:
给出一个数据流,我们需要在此数据流中随机选取 k 个数。由于这个数据流的长度很大,因此需要边遍历边处理,而不能将其一次性全部加载到内存。
请写出一个随机选择算法,使得数据流中所有数据被等概率选中。
2. 算法流程描述:
- 构建一个大小为
N的数组,初始将数据流中的前N个元素放入数组中,N即蓄水池的大小。 - 从数据流的第
N+1个元素开始,在[1, i]之间随机生成一个数rand,其中i表示当前是第几个元素。 - 规定之后的元素以
N/i的概率被选中,即rand>N时表示当前数没有被选中,不做任何操作,反之,当前数被选中,随机替换蓄水池中的一个元素。 - 最终返回蓄水池中的所有元素信息。
通过上述流程之后,能够保证对于数据流中的元素能够被等概率的抽取到。该算法的核心在于先以某一种概率选取数,并在后续过程以另一种概率换掉之前已经被选中的数。因此实际上每个数被最终选中的概率都是被选中的概率 * 不被替换的概率。
3. 举例说明:
假设蓄水池大小N=10,总共数据流元素为1729个,
(1)计算第1729号元素到来时,3号元素依旧存货的概率,即3号元素最终被选中的概率
10号元素之前,3号元素存活的概率为1,
11号元素到来时,3号元素被淘汰的概率计算公式为:11号元素被选中的概率3号元素被选中淘汰的概率,进而可以得到11号元素到来时3号元素存货的概率P(11)为
,
12号元素到来时,3号元素被淘汰的概率计算公式为:12号元素被选中的概率3号元素被选中淘汰的概率,进而可以得到12号元素到来时3号元素存货的概率P(12)为
,
13号元素到来时,3号元素被淘汰的概率计算公式为:13号元素被选中的概率3号元素被选中淘汰的概率,进而可以得到12号元素到来时3号元素存货的概率P(13)为
,
…….依次类推
1729号元素到来时,3号元素被淘汰的概率计算公式为:1729号元素被选中的概率3号元素被选中淘汰的概率,即,进而可以得到1729号元素到来时3号元素存活的概率P(1729)为
根据条件概率公式,当1729号元素到来时3号元素存活的概率为:=第3步被选中概率 不被 11 号元素替换的概率 不被 12 替换的概率 … 不被 1729替换的概率,
等价于 1 (1 - 被 11 替换的概率) (1 - 被12 替换的概率) … (1 - 被 1729替换的概率),
等价于
(2)计算第1729号元素到来时,13号元素依旧存货的概率,即3号元素最终被选中的概率
13>10,当13号元素到来时,13号元素被选中的概率为
14号元素到来时,13号元素被淘汰的概率计算公式为:14号元素被选中的概率13号元素被选中淘汰的概率,进而可以得到14号元素到来时13号元素存活的概率P(14)为
,
15号元素到来时,13号元素被淘汰的概率计算公式为:15号元素被选中的概率13号元素被选中淘汰的概率,进而可以得到15号元素到来时13号元素存活的概率P(15)为
,
…….依次类推
1729号元素到来时,13号元素被淘汰的概率计算公式为1729号元素被选中的概率13号元素被选中淘汰的概率,即进而可以得到1729号元素到来时13号元素存活的概率P(1729)为
根据条件概率公式,当1729号元素到来时13号元素存活的概率为:=第13步被选中概率 不被 14号元素替换的概率 不被 15号元素替换的概率 … 不被 1729号元素替换的概率,
等价于 1 (1 - 被 14 替换的概率) (1 - 被15 替换的概率) * … (1 - 被 1729替换的概率),
等价于
根据上述计算结果可以得知,无论时10号元素之前的,还是10号元素之后的,最后被选中的概率都是相等的。因此,根据该算法可以保证数据流中的数据能够被等概率选中。
4. 代码实现:
public static class RandomBox {private int[] bag; // 蓄水池具体元素值private int N; // 蓄水池大小private int count; // 统计每个位置被选中的次数,测试时使用该参数public RandomBox(int capacity) {bag = new int[capacity];N = capacity;count = 0;}// 等概率返回1-max中的任意数private int rand(int max) {return (int) (Math.random() * max) + 1; //}// 当前数num到达public void add(int num) {count++;if (count <= N) {bag[count - 1] = num;} else {if (rand(count) <= N) {// 随机数bag[rand(N) - 1] = num;}}}// 选择结果public int[] choices() {int[] ans = new int[N];for (int i = 0; i < N; i++) {ans[i] = bag[i];}return ans;}}
测试代码:
// 100个人中随机选择十个作为幸运观众,测试50000次,统计100个观众成为候选人的次数System.out.println("hello");int all = 100;int choose = 10;int testTimes = 50000;int[] counts = new int[all + 1];int[] ans = new int[all + 1];for (int i = 0; i < testTimes; i++) {RandomBox box = new RandomBox(choose);for (int num = 1; num <= all; num++) {box.add(num);}ans = box.choices();for (int j = 0; j < ans.length; j++) {counts[ans[j]]++;}}for (int i = 1; i < counts.length; i++) {System.out.println(i + " times : " + counts[i]);}// 幸运观众for (int i = 0; i < ans.length; i++){System.out.println(ans[i]);}
测试结果如下:
可以发现,1-100个人中每个人被选中的频率相差不大,同时返回最后选中的是为幸运观众编号。
1 times : 51062 times : 50323 times : 50554 times : 49635 times : 50226 times : 50807 times : 50368 times : 51039 times : 498710 times : 492611 times : 496012 times : 507313 times : 498414 times : 502015 times : 512416 times : 506617 times : 499318 times : 503519 times : 511020 times : 490721 times : 499622 times : 506723 times : 500224 times : 496425 times : 500826 times : 509727 times : 488428 times : 508229 times : 501930 times : 502231 times : 489032 times : 489833 times : 492534 times : 490135 times : 503836 times : 489337 times : 510938 times : 496339 times : 493040 times : 488641 times : 491042 times : 498543 times : 491144 times : 504045 times : 499746 times : 502647 times : 494348 times : 516549 times : 485850 times : 503151 times : 493052 times : 501953 times : 507454 times : 506255 times : 501556 times : 495357 times : 504958 times : 492559 times : 505360 times : 513361 times : 503662 times : 493563 times : 505964 times : 500565 times : 497366 times : 492167 times : 502668 times : 503569 times : 501970 times : 501971 times : 506472 times : 491773 times : 497274 times : 495575 times : 504676 times : 487477 times : 498978 times : 495879 times : 511280 times : 486481 times : 503982 times : 509383 times : 496884 times : 493385 times : 493186 times : 496487 times : 509688 times : 494089 times : 493590 times : 492691 times : 501792 times : 497893 times : 504294 times : 506195 times : 492896 times : 508497 times : 497698 times : 503399 times : 5027100 times : 5015193558942736713996
5. 相关题目和应用:
(1)应用场景:
随机抽奖类的工程问题,如在2022年5月1日00:00:00至2022年5月3日23:59:59之间登陆的用户随机抽取100名用户作为幸运观众,发放礼品。这就可以用到蓄水池抽样算法.
(2)相关题目:
