这个问题通常被视为两种竞争算法之间的选择:
根据k和N之间的关系进行选择:如果k足够大,则使用策略FY;否则,策略 HT。论点是,如果k 相对于n 来说很小,那么维护一个大小为n 的数组就是浪费空间,并且会产生很大的初始化成本。另一方面,随着k 接近n,越来越多的随机数需要被丢弃,最终生成新值将非常缓慢。
当然,您可能事先不知道将要求的样品数量。在这种情况下,你可能会悲观地选择 FY,或者乐观地选择 HT,并希望最好。
其实并没有真正需要权衡取舍,因为 FY 算法可以用哈希表高效实现。无需初始化 N 整数数组。相反,哈希表仅用于存储数组中其值与其索引不对应的元素。
(以下描述使用基于 1 的索引;这似乎是问题所要寻找的。希望它没有完全错误。因此它生成范围 [1, N] 内的数字。从这里上,我使用k 表示迄今为止已请求的样本数量,而不是最终将被请求的数量。)
在增量 FY 算法的每个点,从范围 [k, N] 中随机选择单个索引 r。然后索引k 和r 处的值被交换,之后k 为下一次迭代递增。
作为一个效率点,请注意,我们实际上并不需要进行交换:我们只需生成 r 的值,然后将 r 的值设置为 k 的值。我们将永远不会再查看索引 k 处的值,因此没有必要更新它。
最初,我们使用哈希表模拟数组。为了在(虚拟)数组中查找索引i 处的值,我们查看哈希表中是否存在i:如果存在,那就是索引i 处的值。否则索引i 的值就是i 本身。我们从一个空的哈希表开始(这样可以节省初始化成本),它表示一个数组,其每个索引处的值都是索引本身。
要进行 FY 迭代,对于每个样本索引 k,我们如上所述生成一个随机索引 r,产生该索引处的值,然后将索引 r 处的值设置为索引处的值 @ 987654349@。这正是上面描述的 FY 的过程,除了我们查找值的方式。
这需要两次哈希表查找,一次插入(在已查找的索引处,理论上可以更快地完成),每次迭代生成一次随机数。这比策略 HT 的最佳情况多一次查找,但我们节省了一点,因为我们从不需要循环来产生一个值。 (当我们重新哈希时还有一个小的潜在节省,因为我们可以删除任何小于当前值 k 的键。)
随着算法的进行,哈希表会增长;使用标准的指数重新散列策略。在某些时候,哈希表将达到N-k 整数向量的大小。 (由于哈希表开销,这一点的值将达到k 远小于N,但即使没有开销,这个阈值也会在N/2 处达到。)此时,而不是重新散列,散列用于创建现在非虚拟数组的尾部,该过程比重新散列花费的时间更少,并且永远不需要重复;剩余样本将使用标准增量 FY 算法进行选择。
如果k 最终达到阈值点,则此解决方案比 FY 稍慢,如果 k 永远不会变得足够大以至于拒绝随机数,则它比 HT 稍慢。但在这两种情况下都不会慢很多,并且当k 具有尴尬的价值时,如果永远不会遭受病态的减速。
如果不清楚,这里是一个粗略的 Python 实现:
from random import randint
def sampler(N):
k = 1
# First phase: Use the hash
diffs = {}
# Only do this until the hash table is smallish (See note)
while k < N // 4:
r = randint(k, N)
yield diffs[r] if r in diffs else r
diffs[r] = diffs[k] if k in diffs else k
k += 1
# Second phase: Create the vector, ignoring keys less than k
vbase = k
v = list(range(vbase, N+1))
for i, s in diffs.items():
if i >= vbase:
v[i - vbase] = s
del diffs
# Now we can generate samples until we hit N
while k <= N:
r = randint(k, N)
rv = v[r - vbase]
v[r - vbase] = v[k - vbase]
yield rv
k += 1
注意:N // 4 可能是悲观的;计算正确的值需要对哈希表实现了解太多。如果我真的关心速度,我会用编译语言编写自己的哈希表实现,然后我就会知道:)