你说的对。但是,您的代码没有正确实现算法 R。错误是您的(或编写此代码的人),而不是 Knuth 的 ;-)
引自 Knuth(计算机编程艺术第 2 卷,1998 年第 3 版,第 144 页):
...如果我们事先不知道 N 的值,就会出现问题,因为 N 的精确值在算法 S 中至关重要。假设我们想从文件中随机选择 n 个项目,而不知道确切有多少存在于该文件中。我们可以先遍历并计算记录,然后再进行第二次选择;但通常最好在第一次通过时对原始项目中的 m > n 进行采样,其中 m 远小于 N,因此在第二次通过时只需要考虑 m 个项目。当然,诀窍是这样做的方式是最终结果是原始文件的真正随机样本。
由于我们不知道输入何时结束,我们必须跟踪到目前为止看到的输入记录的随机样本,因此始终为结束做好准备。当我们读取输入时,我们将构建一个“reservoir”,其中仅包含先前样本中出现的记录。前 n 条记录总是进入存储库。当输入第 (t + 1) 条记录时,对于 t>n,我们将在内存中有一个包含 n 个索引的表,指向我们从前 t 条中选择的记录。问题是在 t 增加 1 的情况下保持这种情况,即从现在已知存在的 t + 1 条记录中找到一个新的随机样本。不难看出,我们应该以概率 n/(t + 1) 将新记录包含在新样本中,
因此,以下过程可以完成这项工作:
算法 R(水库采样)。在给定 n > 0 的情况下,从大小 > n 的文件中随机选择 n 条记录。名为“reservoir”的辅助文件包含作为最终样本候选的所有记录。该算法使用不同索引表I [j] 表示 1 < j < n,每个索引都指向存储库中的一个记录。
R1。【初始化】输入前n条记录,复制到库中。对于 1 < j < n,设置I [j] ← j,并设置 t ← m ← n。(如果被采样的文件少于n条记录,当然需要中止算法并报告失败。在此算法期间,索引I [1],...,I [n]指向当前样本;m 是存储库的大小;t 是到目前为止处理的输入记录数。)
R2。[文件结束?] 如果没有更多记录要输入,转到步骤 R6。
R3。[生成并测试。] 将 t 增加 1,然后生成一个介于 1 和 t(含)之间的随机整数 M。如果 M > n,则转到 R5。
R4。【添加到reservoir.】将输入文件的下一条记录复制到reservoir,m加1,设置I[M]←m。(先前由 I[M] 指向的记录在样本中被新记录替换。)回到 R2。
R5。[Skip.] 跳过输入文件的下一条记录(不包含在库中),返回步骤R2。
R6。[第二遍] 对I表条目进行排序,使得I [1] < ... < I [n]; 然后通过水库,将具有这些索引的记录复制到用于保存最终样本的输出文件中。
算法 R 的伪代码如下所示:
for j= 1 to n
Reservoir[j]= File.GetNext()
I[j]= j
t=n // number of input records so far
m=n // size of the reservoir
while not File.EOF()
x= File.GetNext()
t++
M= Random(1..t)
if (M<=n)
m++
Reservoir[m]= x
I[M]= m
Sort(I[1..n])
for j= 1 to n
Output[j]= Reservoir[I[j]]