【发布时间】:2010-09-08 10:01:49
【问题描述】:
假设我有一个长度为N 的数字的链接列表。 N 很大,我事先不知道N 的确切值。
我怎样才能最有效地编写一个函数,从列表中完全返回k 随机数?
【问题讨论】:
标签: algorithm language-agnostic list
假设我有一个长度为N 的数字的链接列表。 N 很大,我事先不知道N 的确切值。
我怎样才能最有效地编写一个函数,从列表中完全返回k 随机数?
【问题讨论】:
标签: algorithm language-agnostic list
有一个非常好的和有效的算法使用称为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>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) 概率的每个元素,为我们提供了必要的归纳基础案例。
参考文献
【讨论】:
I 只是一个结构,我们将从中提取要采样的元素。这可以是队列、链表、数组、向量等。然而,由于N 可能非常大,该算法只查看每个项目一次,然后出于所有实际目的将其丢弃。由于该算法也永远不会超越当前元素,因此将此结构称为队列是合适的。
s 可以是任意数量的元素。但是,一旦开始对流进行采样,就无法更改 s 的大小并保证所有元素都以相等的概率出现在输出中。因此,选择s 的大小,使其成为您需要的最大随机元素数,然后从s 中随机抽取您需要的元素。这有帮助吗?
如果您不知道列表的长度,那么您必须完整地遍历它以确保随机选择。我在这种情况下使用的方法是 Tom Hawtin (54070) 描述的方法。在遍历列表时,您保留k 元素,这些元素构成您的随机选择。 (最初,您只需添加遇到的第一个 k 元素。)然后,以 k/i 的概率,您将选择中的随机元素替换为列表的第 ith 元素(即您所在的元素,在那一刻)。
很容易证明这给出了随机选择。在看到 m 元素 (m > 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)。
【讨论】:
这称为Reservoir Sampling 问题。简单的解决方案是为列表的每个元素分配一个随机数,如您所见,然后保持顶部(或底部)k 个元素按随机数排序。
【讨论】:
你为什么不能做类似的事情
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;
我确定你的意思不是那么简单,所以你能进一步说明吗?
【讨论】:
好吧,您至少需要在运行时知道 N 是多少,即使这涉及对列表进行额外的传递以计算它们。最简单的算法是在 N 中选择一个随机数并删除该项目,重复 k 次。或者,如果允许返回重复数字,请不要删除该项目。
除非您有非常大的 N 和非常严格的性能要求,否则此算法以 O(N*k) 复杂度运行,这应该是可以接受的。
编辑:没关系,Tom Hawtin 的方法要好得多。先选择随机数,然后遍历列表一次。我认为,理论上的复杂性相同,但预期的运行时间要好得多。
【讨论】:
我建议:首先找到你的 k 个随机数。对它们进行排序。然后遍历链表和你的随机数一次。
如果您不知何故不知道链表的长度(如何?),那么您可以将第一个 k 抓取到一个数组中,然后对于节点 r,在 [0, r) 中生成一个随机数,如果即小于 k,替换数组的第 r 项。 (不完全相信没有偏见......)
除此之外:“如果我是你,我不会从这里开始。”您确定链表适合您的问题吗?有没有更好的数据结构,比如好的老式扁平数组列表。
【讨论】: