假设我有一个长度为N
的数字链表。由于N
非常大,我事先不知道确切的值。
我该如何编写最有效的函数,以从列表中返回k
个完全随机的数?
假设我有一个长度为N
的数字链表。由于N
非常大,我事先不知道确切的值。
我该如何编写最有效的函数,以从列表中返回k
个完全随机的数?
有一个非常好的、高效的算法可以使用一种叫做“蓄水池抽样”的方法来实现。
让我先给你讲一下它的历史:
Knuth在他1997年版的《计算机编程艺术》第二卷《半数值算法》的第144页中将这个算法称为R算法,并在那里提供了一些代码。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()
R
中(即,没有偏差)。此外,R
始终包含算法考虑的元素的公平和代表性样本。这意味着您可以将其用作在线算法。
为什么这有效?
McLeod和Bellhouse(1983)使用组合数学提供了一个证明。它很漂亮,但在这里重建它可能有点困难。因此,我生成了一种更容易解释的替代证明。s
个元素的集合,并且我们已经看到了n>s
个元素。s
个元素已经分别以s/n
的概率被选择。n+1
的概率为s/(n+1)
。1/s
。n+1
结果集中,来自n
结果集的元素被替换的概率为(1/s)*s/(n+1)=1/(n+1)
。相反地,元素不被替换的概率是1-1/(n+1)=n/(n+1)
。n+1
结果集包含一个元素,如果它是n
结果集的一部分且未被替换---这个概率是(s/n)*n/(n+1)=s/(n+1)
---或者如果选择了该元素---概率为s/(n+1)
。这被称为蓄水池抽样问题。简单的解决方案是在看到列表中的每个元素时,为其分配一个随机数,然后按照随机数的顺序保留前(或后)k个元素。
如果你不知道列表的长度,那么你将需要完全遍历它以确保随机挑选。我在这种情况下使用的方法是由Tom Hawtin描述的方法(54070)。当遍历列表时,你保留k
个元素,这些元素形成了你的随机选择。 (最初,您只需添加遇到的前k
个元素即可。) 然后,以k/i
的概率,您将用列表的第i
个元素(即您此时所在的元素)替换您选择的一个随机元素。
很容易证明这会给出一个随机选择。在看到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)
。
为什么你不能做类似这样的事情呢?
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的方法更好。先选择随机数,然后遍历列表一次。我认为理论复杂度相同,但期望运行时间更短。
s
可以是任意数量的元素。但是一旦您开始对流进行采样,您就不能更改s
的大小并保证所有元素以相等概率出现在输出中。因此,请选择s
的大小,使其是您需要的随机元素的最大数量,然后从s
中随机选择您需要的元素。这有帮助吗? - Richard