面试谈的算法

1. 问题模型描述:

给出一个数据流,我们需要在此数据流中随机选取 k 个数。由于这个数据流的长度很大,因此需要边遍历边处理,而不能将其一次性全部加载到内存。
请写出一个随机选择算法,使得数据流中所有数据被等概率选中。

2. 算法流程描述:

  1. 构建一个大小为N的数组,初始将数据流中的前N个元素放入数组中,N即蓄水池的大小。
  2. 从数据流的第N+1个元素开始,在[1, i]之间随机生成一个数rand,其中i表示当前是第几个元素。
  3. 规定之后的元素以 N/i 的概率被选中,即rand>N时表示当前数没有被选中,不做任何操作,反之,当前数被选中,随机替换蓄水池中的一个元素。
  4. 最终返回蓄水池中的所有元素信息。

通过上述流程之后,能够保证对于数据流中的元素能够被等概率的抽取到。该算法的核心在于先以某一种概率选取数,并在后续过程以另一种概率换掉之前已经被选中的数。因此实际上每个数被最终选中的概率都是被选中的概率 * 不被替换的概率

3. 举例说明:

假设蓄水池大小N=10,总共数据流元素为1729个,
(1)计算第1729号元素到来时,3号元素依旧存货的概率,即3号元素最终被选中的概率
10号元素之前,3号元素存活的概率为1,
11号元素到来时,3号元素被淘汰的概率计算公式为:11号元素被选中的概率3号元素被选中淘汰的概率
19. 蓄水池抽样算法 - 图1,进而可以得到11号元素到来时3号元素存货的概率P(11)为19. 蓄水池抽样算法 - 图2
12号元素到来时,3号元素被淘汰的概率计算公式为:12号元素被选中的概率
3号元素被选中淘汰的概率
19. 蓄水池抽样算法 - 图3,进而可以得到12号元素到来时3号元素存货的概率P(12)为19. 蓄水池抽样算法 - 图4
13号元素到来时,3号元素被淘汰的概率计算公式为:13号元素被选中的概率3号元素被选中淘汰的概率
19. 蓄水池抽样算法 - 图5,进而可以得到12号元素到来时3号元素存货的概率P(13)为19. 蓄水池抽样算法 - 图6
…….依次类推
1729号元素到来时,3号元素被淘汰的概率计算公式为:1729号元素被选中的概率
3号元素被选中淘汰的概率,即19. 蓄水池抽样算法 - 图7,进而可以得到1729号元素到来时3号元素存活的概率P(1729)为19. 蓄水池抽样算法 - 图8
根据条件概率公式,当1729号元素到来时3号元素存活的概率为
19. 蓄水池抽样算法 - 图9=第3步被选中概率 不被 11 号元素替换的概率 不被 12 替换的概率 … 不被 1729替换的概率,
等价于 1
(1 - 被 11 替换的概率) (1 - 被12 替换的概率) … (1 - 被 1729替换的概率),
等价于19. 蓄水池抽样算法 - 图10
19. 蓄水池抽样算法 - 图11
(2)计算第1729号元素到来时,13号元素依旧存货的概率,即3号元素最终被选中的概率
13>10,当13号元素到来时,13号元素被选中的概率为 19. 蓄水池抽样算法 - 图12
14号元素到来时,13号元素被淘汰的概率计算公式为:14号元素被选中的概率13号元素被选中淘汰的概率19. 蓄水池抽样算法 - 图13,进而可以得到14号元素到来时13号元素存活的概率P(14)为19. 蓄水池抽样算法 - 图14
15号元素到来时,13号元素被淘汰的概率计算公式为:15号元素被选中的概率
13号元素被选中淘汰的概率19. 蓄水池抽样算法 - 图15,进而可以得到15号元素到来时13号元素存活的概率P(15)为19. 蓄水池抽样算法 - 图16
…….依次类推
1729号元素到来时,13号元素被淘汰的概率计算公式为1729号元素被选中的概率13号元素被选中淘汰的概率,即19. 蓄水池抽样算法 - 图17进而可以得到1729号元素到来时13号元素存活的概率P(1729)为19. 蓄水池抽样算法 - 图18
根据条件概率公式,当1729号元素到来时13号元素存活的概率为
19. 蓄水池抽样算法 - 图19=第13步被选中概率
不被 14号元素替换的概率 不被 15号元素替换的概率 … 不被 1729号元素替换的概率,
等价于 1 (1 - 被 14 替换的概率) (1 - 被15 替换的概率) * … (1 - 被 1729替换的概率),
等价于19. 蓄水池抽样算法 - 图20
19. 蓄水池抽样算法 - 图21

根据上述计算结果可以得知,无论时10号元素之前的,还是10号元素之后的,最后被选中的概率都是相等的。因此,根据该算法可以保证数据流中的数据能够被等概率选中。

4. 代码实现:

  1. public static class RandomBox {
  2. private int[] bag; // 蓄水池具体元素值
  3. private int N; // 蓄水池大小
  4. private int count; // 统计每个位置被选中的次数,测试时使用该参数
  5. public RandomBox(int capacity) {
  6. bag = new int[capacity];
  7. N = capacity;
  8. count = 0;
  9. }
  10. // 等概率返回1-max中的任意数
  11. private int rand(int max) {
  12. return (int) (Math.random() * max) + 1; //
  13. }
  14. // 当前数num到达
  15. public void add(int num) {
  16. count++;
  17. if (count <= N) {
  18. bag[count - 1] = num;
  19. } else {
  20. if (rand(count) <= N) {
  21. // 随机数
  22. bag[rand(N) - 1] = num;
  23. }
  24. }
  25. }
  26. // 选择结果
  27. public int[] choices() {
  28. int[] ans = new int[N];
  29. for (int i = 0; i < N; i++) {
  30. ans[i] = bag[i];
  31. }
  32. return ans;
  33. }
  34. }

测试代码:

  1. // 100个人中随机选择十个作为幸运观众,测试50000次,统计100个观众成为候选人的次数
  2. System.out.println("hello");
  3. int all = 100;
  4. int choose = 10;
  5. int testTimes = 50000;
  6. int[] counts = new int[all + 1];
  7. int[] ans = new int[all + 1];
  8. for (int i = 0; i < testTimes; i++) {
  9. RandomBox box = new RandomBox(choose);
  10. for (int num = 1; num <= all; num++) {
  11. box.add(num);
  12. }
  13. ans = box.choices();
  14. for (int j = 0; j < ans.length; j++) {
  15. counts[ans[j]]++;
  16. }
  17. }
  18. for (int i = 1; i < counts.length; i++) {
  19. System.out.println(i + " times : " + counts[i]);
  20. }
  21. // 幸运观众
  22. for (int i = 0; i < ans.length; i++){
  23. System.out.println(ans[i]);
  24. }

测试结果如下:
可以发现,1-100个人中每个人被选中的频率相差不大,同时返回最后选中的是为幸运观众编号。

  1. 1 times : 5106
  2. 2 times : 5032
  3. 3 times : 5055
  4. 4 times : 4963
  5. 5 times : 5022
  6. 6 times : 5080
  7. 7 times : 5036
  8. 8 times : 5103
  9. 9 times : 4987
  10. 10 times : 4926
  11. 11 times : 4960
  12. 12 times : 5073
  13. 13 times : 4984
  14. 14 times : 5020
  15. 15 times : 5124
  16. 16 times : 5066
  17. 17 times : 4993
  18. 18 times : 5035
  19. 19 times : 5110
  20. 20 times : 4907
  21. 21 times : 4996
  22. 22 times : 5067
  23. 23 times : 5002
  24. 24 times : 4964
  25. 25 times : 5008
  26. 26 times : 5097
  27. 27 times : 4884
  28. 28 times : 5082
  29. 29 times : 5019
  30. 30 times : 5022
  31. 31 times : 4890
  32. 32 times : 4898
  33. 33 times : 4925
  34. 34 times : 4901
  35. 35 times : 5038
  36. 36 times : 4893
  37. 37 times : 5109
  38. 38 times : 4963
  39. 39 times : 4930
  40. 40 times : 4886
  41. 41 times : 4910
  42. 42 times : 4985
  43. 43 times : 4911
  44. 44 times : 5040
  45. 45 times : 4997
  46. 46 times : 5026
  47. 47 times : 4943
  48. 48 times : 5165
  49. 49 times : 4858
  50. 50 times : 5031
  51. 51 times : 4930
  52. 52 times : 5019
  53. 53 times : 5074
  54. 54 times : 5062
  55. 55 times : 5015
  56. 56 times : 4953
  57. 57 times : 5049
  58. 58 times : 4925
  59. 59 times : 5053
  60. 60 times : 5133
  61. 61 times : 5036
  62. 62 times : 4935
  63. 63 times : 5059
  64. 64 times : 5005
  65. 65 times : 4973
  66. 66 times : 4921
  67. 67 times : 5026
  68. 68 times : 5035
  69. 69 times : 5019
  70. 70 times : 5019
  71. 71 times : 5064
  72. 72 times : 4917
  73. 73 times : 4972
  74. 74 times : 4955
  75. 75 times : 5046
  76. 76 times : 4874
  77. 77 times : 4989
  78. 78 times : 4958
  79. 79 times : 5112
  80. 80 times : 4864
  81. 81 times : 5039
  82. 82 times : 5093
  83. 83 times : 4968
  84. 84 times : 4933
  85. 85 times : 4931
  86. 86 times : 4964
  87. 87 times : 5096
  88. 88 times : 4940
  89. 89 times : 4935
  90. 90 times : 4926
  91. 91 times : 5017
  92. 92 times : 4978
  93. 93 times : 5042
  94. 94 times : 5061
  95. 95 times : 4928
  96. 96 times : 5084
  97. 97 times : 4976
  98. 98 times : 5033
  99. 99 times : 5027
  100. 100 times : 5015
  101. 19
  102. 35
  103. 58
  104. 94
  105. 27
  106. 36
  107. 7
  108. 13
  109. 9
  110. 96

5. 相关题目和应用:

(1)应用场景:
随机抽奖类的工程问题,如在2022年5月1日00:00:00至2022年5月3日23:59:59之间登陆的用户随机抽取100名用户作为幸运观众,发放礼品。这就可以用到蓄水池抽样算法.

(2)相关题目: