假设我有一个长度为N
的数字的链表。 N
非常大,我事先并不知道N
的确切价值。
如何才能最有效地编写一个从列表中完全返回随机数的k
函数?
答案 0 :(得分:37)
使用称为储层采样的方法,有一种非常好的高效算法。
首先,让我给你历史:
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()
请注意,我专门编写了代码以避免指定输入的大小。这是该算法的一个很酷的属性:您可以在不需要事先知道输入大小的情况下运行它,并且仍然向您保证您遇到的每个元素都有相同的概率结束{ {1}}(也就是说,没有偏见)。此外,R
包含算法始终考虑的元素的公平且有代表性的样本。这意味着您可以将其用作online algorithm。
为什么这样做?
McLeod和Bellhouse(1983)提供了使用组合数学的证明。它很漂亮,但在这里重建它会有点困难。因此,我已经生成了一个更容易解释的替代证明。我们通过归纳证明进行。
假设我们要生成一组R
元素,并且我们已经看到了s
元素。
我们假设我们当前的n>s
元素已经被概率s
选中。
根据算法的定义,我们选择概率为s/n
的元素n+1
。
每个元素已经是我们结果集的一部分,被替换的概率为s/(n+1)
。
1/s
看到的结果集中n
- 看到的结果集中的元素被n+1
- 看到的结果集中的概率(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)
。
算法的定义告诉我们,第一个s
元素会自动包含在结果集的第一个n=s
成员中。因此,n-seen
结果集包含s/n
(= 1)概率的每个元素,为我们提供必要的归纳基础案例。
<强>参考强>
答案 1 :(得分:34)
这称为Reservoir Sampling问题。简单的解决方案是在您看到的时候为列表的每个元素分配一个随机数,然后保持按随机数排序的顶部(或底部)k元素。
答案 2 :(得分:2)
我建议:首先找到你的k个随机数。排序他们。然后遍历链表和随机数一次。
如果你以某种方式不知道链表的长度(如何?),那么你可以将第一个k抓到一个数组中,然后对于节点r,在[0,r)中生成一个随机数,如果小于k,替换数组的rth项。 (并不完全相信不会偏见......)
除此之外:“如果我是你,我不会从这里开始。”你确定链表适合你的问题吗?是不是有更好的数据结构,例如一个好的旧平面数组列表。
答案 3 :(得分:1)
如果你不知道列表的长度,那么你必须完整地遍历它以确保随机选择。我在这种情况下使用的方法是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)
。
答案 4 :(得分:-3)
嗯,你确实需要知道N在运行时至少是什么,即使这涉及对列表进行额外的传递来计算它们。最简单的算法是在N中选择一个随机数并删除该项,重复k次。或者,如果允许返回重复的数字,请不要删除该项目。
除非您有非常大的N和非常严格的性能要求,否则此算法的运行复杂度为O(N*k)
,这应该是可以接受的。
编辑:没关系,Tom Hawtin的方法更好。首先选择随机数,然后遍历列表一次。我认为,相同的理论复杂性,但预期的运行时间要好得多。
答案 5 :(得分:-3)
为什么你不能做像
这样的事情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;
我确信你并不是指那么简单的东西,你可以进一步说明吗?