【问题标题】:Efficiently selecting a set of random elements from a linked list从链表中有效地选择一组随机元素
【发布时间】:2010-09-08 10:01:49
【问题描述】:

假设我有一个长度为N 的数字的链接列表。 N 很大,我事先不知道N 的确切值。

我怎样才能最有效地编写一个函数,从列表中完全返回k 随机数

【问题讨论】:

    标签: algorithm language-agnostic list


    【解决方案1】:

    有一个非常好的和有效的算法使用称为reservoir sampling的方法。

    让我先介绍一下它的历史

    Knuth 在 p 上将此算法称为 R。他 1997 年版的半数值算法(计算机编程艺术的第 2 卷)的第 144 卷,并在那里提供了一些代码。 Knuth 将该算法归功于 Alan G. Waterman。尽管搜索了很长时间,但我还是找不到 Waterman 的原始文档(如果存在),这可能就是为什么您经常会看到 Knuth 被引用为该算法的来源。

    McLeod 和 Bellhouse,1983 年 (1) 提供了比 Knuth 更彻底的讨论以及该算法有效的第一个公开证明(据我所知)。

    Vitter 1985 (2) 回顾了算法 R,然后提出了另外三种提供相同输出但有所不同的算法。他的算法不是选择包含或跳过每个传入元素,而是预先确定要跳过的传入元素的数量。在他的测试中(诚然,这些测试现在已经过时),通过避免随机数生成和对每个传入数字的比较,显着减少了执行时间。

    伪代码中,算法是:

    Let R be the result array of size s
    Let I be an input queue
    
    > Fill the reservoir array
    for j in the range [1,s]:
      R[j]=I.pop()
    
    elements_seen=s
    while I is not empty:
      elements_seen+=1
      j=random(1,elements_seen)       > This is inclusive
      if j<=s:
        R[j]=I.pop()
      else:
        I.pop()
    

    请注意,我专门编写了代码以避免指定输入的大小。这是该算法的一个很酷的特性:您可以运行它而无需事先知道输入的大小,并且它仍然确保您遇到的每个元素都有相同的概率以 @ 结尾987654325@(即没有偏见)。此外,R 包含算法一直考虑的元素的公平和代表性样本。这意味着您可以将其用作online algorithm

    为什么会这样?

    McLeod 和 Bellhouse (1983) 提供了使用组合数学的证明。它很漂亮,但在这里重建它会有点困难。因此,我生成了一个更容易解释的替代证明。

    我们通过归纳证明进行。

    假设我们要生成一组s 元素并且我们已经看到了n&gt;s 元素。

    假设我们当前的s 元素已经以s/n 的概率被选中。

    根据算法的定义,我们选择元素n+1,概率为s/(n+1)

    已经成为我们结果集中一部分的每个元素都有1/s 被替换的概率。

    n-seen 结果集中的元素在n+1-seen 结果集中被替换的概率因此为(1/s)*s/(n+1)=1/(n+1)。反之,一个元素未被替换的概率为1-1/(n+1)=n/(n+1)

    因此,n+1-seen 结果集包含一个元素,如果它是n-seen 结果集的一部分并且未被替换---这个概率是(s/n)*n/(n+1)=s/(n+1)---或者如果该元素被选中——概率为s/(n+1)

    算法的定义告诉我们,第一个s 元素会自动包含在结果集中的第一个n=s 成员中。因此,n-seen 结果集包含具有s/n (=1) 概率的每个元素,为我们提供了必要的归纳基础案例。

    参考文献

    1. McLeod、A. Ian 和 David R. Bellhouse。 “一种用于绘制简单随机样本的便捷算法。”皇家统计学会杂志。系列 C(应用统计)32.2(1983):182-184。 (Link)

    2. Vitter, Jeffrey S. “使用水库进行随机采样”。 ACM 数学软件交易 (TOMS) 11.1 (1985): 37-57。 (Link)

    【讨论】:

    • 这里输入队列是什么意思?
    • @Anoop, I 只是一个结构,我们将从中提取要采样的元素。这可以是队列、链表、数组、向量等。然而,由于N 可能非常大,该算法只查看每个项目一次,然后出于所有实际目的将其丢弃。由于该算法也永远不会超越当前元素,因此将此结构称为队列是合适的。
    • 多么棒的答案!如果我一次只想要一个随机元素怎么办?如果 s=1 并且具有相同的随机性,可以吗?
    • @tamara, s 可以是任意数量的元素。但是,一旦开始对流进行采样,就无法更改 s 的大小并保证所有元素都以相等的概率出现在输出中。因此,选择s 的大小,使其成为您需要的最大随机元素数,然后从s 中随机抽取您需要的元素。这有帮助吗?
    • 我的想法也朝着这个方向发展,目前正在尝试实施它。谢谢!
    【解决方案2】:

    如果您不知道列表的长度,那么您必须完整地遍历它以确保随机选择。我在这种情况下使用的方法是 Tom Hawtin (54070) 描述的方法。在遍历列表时,您保留k 元素,这些元素构成您的随机选择。 (最初,您只需添加遇到的第一个 k 元素。)然后,以 k/i 的概率,您将选择中的随机元素替换为列表的第 ith 元素(即您所在的元素,在那一刻)。

    很容易证明这给出了随机选择。在看到 m 元素 (m &gt; k) 之后,我们发现列表的前 m 元素中的每一个都是您随机选择的一部分,概率为 k/m。这最初成立是微不足道的。然后对于每个元素m+1,您将其放入您的选择中(替换随机元素),概率为k/(m+1)。您现在需要证明所有其他元素也有k/(m+1) 被选中的概率。我们知道概率是k/m * (k/(m+1)*(1-1/k) + (1-k/(m+1)))(即元素在列表中的概率乘以它仍然存在的概率)。通过微积分,您可以直接证明这等于 k/(m+1)

    【讨论】:

    • 这应该是选择的答案
    【解决方案3】:

    这称为Reservoir Sampling 问题。简单的解决方案是为列表的每个元素分配一个随机数,如您所见,然后保持顶部(或底部)k 个元素按随机数排序。

    【讨论】:

    • 知道为什么这被称为reservoir采样吗?
    • @Lazer:我不确定Vitter (PDF) 是否最初创造了这个术语,但他将问题表述为“从 池中选择 n 条记录的随机样本而不进行替换 N 条记录...”(强调我的)。他在第 2 节中给出了一个定义,开头是“任何存储库算法的第一步都是将文件的前 n 条记录放入“存储库”。......”
    • 这很优雅。然而,我有一个疑问。在分配给我们的流的那些随机数中有重复项怎么样。假设第 k 个和第 (k+1) 个元素按排序顺序分配了相同的数字,并且仅选择前 K 个元素将“取决于”我们打破平局的方式..对吗?请你澄清一下
    • @phani 是的,如果您在某个范围内使用随机整数,这可能是个问题。如果您只使用随机双精度,则重复对样本随机性的影响应该可以忽略不计。
    • @BilltheLizard True 表示任意精度的实数,但doubles 使用几位作为指数,因此使用普通的 64 位整数可能会更好。
    【解决方案4】:

    你为什么不能做类似的事情

    List GetKRandomFromList(List input, int k)
      List ret = new List();
      for(i=0;i<k;i++)
        ret.Add(input[Math.Rand(0,input.Length)]);
      return ret;
    

    我确定你的意思不是那么简单,所以你能进一步说明吗?

    【讨论】:

    • 效率低下的原因是,在第 4 行计数中获取 input[x] 的值对于链表来说非常昂贵,因为您必须遍历所有项目直到 x 到到达那里 - 你的解决方案会这样做 k 次。正如此处的其他答案所指出的那样,您只需通过列表即可完成。
    【解决方案5】:

    好吧,您至少需要在运行时知道 N 是多少,即使这涉及对列表进行额外的传递以计算它们。最简单的算法是在 N 中选择一个随机数并删除该项目,重复 k 次。或者,如果允许返回重复数字,请不要删除该项目。

    除非您有非常大的 N 和非常严格的性能要求,否则此算法以 O(N*k) 复杂度运行,这应该是可以接受的。

    编辑:没关系,Tom Hawtin 的方法要好得多。先选择随机数,然后遍历列表一次。我认为,理论上的复杂性相同,但预期的运行时间要好得多。

    【讨论】:

      【解决方案6】:

      我建议:首先找到你的 k 个随机数。对它们进行排序。然后遍历链表和你的随机数一次。

      如果您不知何故不知道链表的长度(如何?),那么您可以将第一个 k 抓取到一个数组中,然后对于节点 r,在 [0, r) 中生成一个随机数,如果即小于 k,替换数组的第 r 项。 (不完全相信没有偏见......)

      除此之外:“如果我是你,我不会从这里开始。”您确定链表适合您的问题吗?有没有更好的数据结构,比如好的老式扁平数组列表。

      【讨论】:

      • 嗨,Tom - 抱歉,我不太明白 - “首先找到你的 k 个随机数”是什么意思。目标是找到从列表中取出的 k 个随机数 - 或者我误解了什么。
      • @TomHawtin,没有偏见:见我的answer
      猜你喜欢
      • 2012-01-27
      • 2012-08-30
      • 2023-01-11
      • 1970-01-01
      • 1970-01-01
      • 2014-07-23
      • 1970-01-01
      相关资源
      最近更新 更多