想象一个标准的置换函数,它取一个整数并以随机排列的形式返回前N个自然数的向量。如果你只需要k(< = N),但事先不知道k,你还需要进行O(N)代排列吗?是否有比以下更好的算法:
for x in permute(N):
if f(x):
break
我正在想象一个API,例如:
p = permuter(N)
for x = p.next():
if f(x):
break
初始化为O(1)(包括内存分配)。
答案 0 :(得分:3)
这个问题通常被视为两种竞争算法之间的选择:
策略风云:Fisher-Yates shuffle的变体,其中对每个所需数字执行一个洗牌步骤,
策略HT:将所有生成的数字保存在哈希表中。在每个步骤中,产生随机数,直到找到不在哈希表中的数字。
根据k
和N
之间的关系执行选择:如果k
足够大,则使用策略FY;否则,策略HT。争论的焦点是,如果k
相对于n
较小,则维护大小为n
的数组会浪费空间,并产生较大的初始化成本。另一方面,当k
接近n
时,需要丢弃越来越多的随机数,并且最终产生新值将非常缓慢。
当然,您可能事先不知道要求的样本数量。在这种情况下,你可能会悲观地选择FY,或乐观地选择HT,并希望最好。
事实上,没有真正需要权衡,因为可以使用哈希表有效地实现FY算法。无需初始化N
整数数组。相反,哈希表用于仅存储其值与其索引不对应的数组的元素。
(以下描述使用基于1的索引;这似乎是问题所寻求的。希望它没有完整的错误。因此它会生成[1, N]
范围内的数字。从这里开始,我使用k
来获取迄今为止请求的样本数,而不是最终请求的数量。)
在增量FY算法的每个点,从范围r
中随机选择单个索引[k, N]
。然后交换索引k
和r
的值,之后k
递增以进行下一次迭代。
作为效率点,请注意我们并不需要进行交换:我们只需在r
处生成值,然后将r
处的值设置为{{{ 1}}。我们永远不会再查看索引k
的值,因此没有必要更新它。
最初,我们使用哈希表模拟数组。要查找(虚拟)数组中索引k
的值,我们会看到哈希表中是否存在i
:如果是,那就是索引i
处的值。否则,索引i
的值本身为i
。我们从一个空的哈希表开始(这节省了初始化成本),它表示一个数组,其每个索引的值都是索引本身。
要进行FY迭代,对于每个样本索引i
,我们生成如上所述的随机索引k
,在该索引处生成值,然后在索引r
处设置值到索引r
的值。这正是上面针对FY所描述的程序,除了我们查找值的方式。
这需要正好两个哈希表查找,一个插入(在已查找的索引中,理论上可以更快地完成),并且每次迭代生成一个随机数。这是一个比策略HT最好的情况下的查找,但我们有一点节省,因为我们永远不需要循环来产生一个值。 (当我们重新散列时,还有另一个小的潜在节省,因为我们可以删除任何小于k
的当前值的键。)
随着算法的进行,哈希表将增长;使用标准指数重组策略。在某些时候,哈希表将达到k
整数向量的大小。 (由于哈希表开销,此点的值将N-k
远小于k
,但即使没有开销,也会在N
达到此阈值。)此时,散列用于创建现在非虚拟数组的尾部,而不是重新散列,这个过程花费的时间比重新散列更少,并且永远不需要重复;将使用标准增量FY算法选择剩余样本。
如果N/2
最终达到阈值点,此解决方案略慢于FY,如果k
永远不会变得足够大以致于随机数被拒绝,则它稍微慢于HT。但是在任何一种情况下它都不会慢得多,并且如果k
具有尴尬的价值,从来不会遇到病态的减速。
如果不清楚,这是一个粗略的Python实现:
k
注意: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
可能是悲观的;计算正确的值需要了解太多关于哈希表的实现。如果我真的关心速度,我会用编译语言编写自己的哈希表实现,然后我会知道:)