【问题标题】:Efficient random sampling from a huge list从庞大的列表中进行有效的随机抽样
【发布时间】:2025-11-28 04:40:01
【问题描述】:

我有一个包含大量值 (53,000,000+) 的数据文件,我想提取这些值的 n 个随机子集(例如,2,000,000)。我实现了一个 Perl 脚本,它将列表拉入内存,使用 Fisher-Yates method 对数组进行混洗,然后打印出混洗列表中的第一个 n 值。然而,这个洗牌过程需要很多时间,即使是在更小的测试集(50,000 个值)上也是如此。

我正在寻找一种更高效、可扩展的方法来识别大量值的随机子集并将其打印出来。有什么建议吗?

更新:根据答案和更多搜索,正确的术语似乎是“随机抽样”。

【问题讨论】:

  • 你如何交换)? I/O 是瓶颈还是洗牌?也许一些代码也会有所帮助。
  • @delnan 我尝试了链接线程中描述的两种方法。 I/O 绝对不是问题。它会很快加载到内存中,但随后会在洗牌步骤上花费 很多 时间。它永远不会完成并开始打印。既然我已经尝试过了,我认为洗牌方法对于这么多值的效率不够高,所以我可能对替代方法更感兴趣。
  • 您需要数据有多随机?您可能可以使用一个循环,该循环通过随机索引跳转,然后在检索元素后将其标记为“已使用”以防止重复。

标签: performance random sampling random-sample


【解决方案1】:

详细说明上面的 aix 答案,从一系列项目中选择k,一次读取一个项目。将第一个 k 项目保留在一组 S 中。

现在在阅读m-th 项目Im>k 现在)时,以概率k/m 保留它。如果你保留它,从S中均匀随机选择一个项目U,并将U替换为I

证明这会以相等的概率产生大小为k 的所有子集,这是基于对m 的归纳。注意,你不需要提前知道n(项目总数),每一步的S是合适的。该算法是“流式传输”的 - 它不需要存储所有项目或进行第二次传递。

【讨论】:

  • 这是一种在线算法,非常适合大小未知的流。但是这里的大小是已知的,所以你可以做得更好
  • 您可以做得更好,因为您可以在预先知道 n 的情况下减少对随机数生成器(k 而不是 n)的调用,并且可以将集合存储在内存中。时间复杂度,两者都是 O(n),在线算法使用 O(1) 空间,而不是 \Theta(n)
  • 不,如果集合已经在内存中,时间复杂度只有 O(k)。如果不是,那么这是内存与速度的权衡,所以从这个意义上说你是对的。
  • 也许从问题陈述中并不清楚,但我事先不知道n——我给出了一个粗略的想法来显示问题的规模。鉴于项目的性质,将整个集合加载到内存中是不可行的。所以我很喜欢这个答案。
【解决方案2】:

首先,检查您的 shuffle 实现。如果实施得当,那应该会给你线性时间。此外,修改算法以在所需数量的元素被打乱后停止:没有必要(实际上和理论上)打乱比实际输出更多的数字。

如果您要求 k 个数字,这将花费您 k 个元素操作。我怀疑你能做得比这更好。

【讨论】:

  • 如果你有足够的内存,这是完成任务的最佳算法
【解决方案3】:

不要洗牌,太贵了。

在 Jon Bentley 的 "Programming Pearls" 中讨论了一个 simple linear algorithm(Bentley 说他是从 Knuth 的 "Seminumerical Algorithms" 中学到的)。请改用此方法。

有一些Perl implementations关于:

这两个 sn-ps 实现了 Algortihm S(3.4.2) 和 Algortihm R(3.4.2) 来自 Knuth 的《编程艺术》。第一个随机选择N个项目 从一个元素数组,并返回一个数组的引用 包含元素。请注意,它不一定会考虑 列表中的所有元素。

第二个随机从一个大小不定的文件中选择N个项目 并返回一个包含所选元素的数组。中的记录 文件被假定为每行,并且这些行在 阅读。这只需要通过列表 1 次。轻微 在 N 的情况下可以进行修改以使用 sn-p 记录会超出内存限制,但这需要 略多于 1 遍(/msg 如果您需要解释)

【讨论】:

    【解决方案4】:

    读取和改组数组会涉及大量不必要的数据移动。

    这里有一些想法:

    One:当你说你需要一个随机子集时,在这种情况下你所说的“随机”到底是什么意思?我的意思是,记录是按任何特定顺序排列的,还是与您尝试随机化的顺序相关?

    因为我的第一个想法是,如果记录没有任何相关顺序,那么您可以通过简单地计算总大小除以样本大小,然后选择每第 n 个记录来获得随机选择。因此,例如,如果您有 5300 万条记录,并且您想要 200 万条样本,则取 5300 万 / 200 万 ~= 26,因此每 26 条记录读取一次。

    二:如果这还不够,更严格的解决方案是生成 200 万个 0 到 5300 万范围内的随机数,确保没有重复。

    Two-A:如果您的样本量与记录总数相比很小,例如如果您只是挑选几百或几千条,我会生成一个包含许多条目的数组,并且对于每个条目,将其与所有以前的条目进行比较以检查重复项。如果它是重复的,请循环并重试,直到找到唯一值。

    Two-B:假设您的数字不仅仅是示例,而是实际值,那么与总人口相比,您的样本量很大。在这种情况下,考虑到现代计算机上的充足内存,您应该能够通过创建一个包含 5300 万个布尔值的数组来有效地执行此操作,这些布尔值初始化为 false,当然每个都代表一个记录。然后循环运行 200 万次。对于每次迭代,生成一个从 0 到 5300 万的随机数。检查数组中对应的布尔值:如果为假,则设置为真。如果为真,则生成另一个随机数,然后重试。

    三:或者等等,考虑到相对较大的百分比,这里有一个更好的主意:计算您想要包含的记录的百分比。然后循环遍历所有记录的计数器。对于每一个,生成一个从 0 到 1 的随机数,并将其与所需的百分比进行比较。如果它更少,请阅读该记录并将其包含在示例中。如果更大,请跳过记录。

    如果获取准确的样本记录数很重要,您可以重新计算每条记录的百分比。例如——为了简单起见,假设您想要 100 条记录中的 10 条:

    你会从 10 / 100 = .1 开始,所以我们生成一个随机数,假设它是 0.04。 .04<.1>

    现在我们重新计算百分比。我们希望在剩余的 99 条记录中再增加 9 条记录,给出 9/99~=.0909 假设我们的随机数是 0.87。这更大,所以我们跳过记录 #2。

    重新计算。在剩余的 98 条记录中,我们仍然需要 9 条记录。所以神奇的数字是 9/98,不管它是什么。等等。

    一旦我们得到尽可能多的记录,未来记录的概率将为零,因此我们永远不会重复。如果我们接近尾声并且没有拾取足够的记录,概率将非常接近 100%。比如,如果我们还需要 8 条记录,而只剩下 8 条记录,那么概率是 8/8=100%,所以我们可以保证获得下一条记录。

    【讨论】: