修正的 Fisher-Yates 算法
可以改进随机播放解决方案,因为您只需随机播放数组的前 k 个元素。但这仍然是 O(n),因为天真的 shuffle 实现需要一个大小为 n 的数组,需要将其初始化为来自的 n 值0 到 n-1.
Initialize value[n] to {0..n-1}
For i from 0 to k-1:
swap(value[i], value[random_in_range(i, n)])
Result is value[0..k-1]
为了改进这一点,我们可以使用一种虚拟数组,由两部分组成:
value:前 k 个元素的数组,将作为结果选择。这被初始化为 {0..k-1}
rest:具有 k 个条目容量的稀疏数据结构(例如哈希表),包含数组的所有剩余条目,这些条目不简单他们自己的索引。最初是空的。
现在我们可以定义从 value 数组中交换元素 i 和 j 的函数(注意:ik,由上述算法保证):
# To swap elements i and j
If j < k:
# Both elements to be swapped are in the selection
tmp = value[i]; value[i] = value[j]; value[j] = tmp
Else If j in rest:
# Element j has been swapped before
tmp = value[i]; value[i] = rest[j]; rest[j] = tmp
Else:
# The value at j is still j, we now add it to the virtual array
rest[j] = value[i]; value[i] = j
对于 k≤n 的任何值,使用 O(k) 空间和时间。
三种算法策略
使用 O(k) 内存的更简单的解决方案是只保留所有选定值的哈希表,并生成值直到哈希表包含 k 值,拒绝重复.
对于小的k,随机选择的元素重复的概率是微不足道的,朴素的哈希表当然是最简单的解决方案。另一方面,如果 k 是 n 的重要部分,则哈希表主要是浪费空间,因为大小为 n的简单位向量> 足以记录已看到的值。对于非常大的k,当样本填满时,拒绝算法将花费太多时间,并且洗牌所需的完整向量并不比用于保存样本。
因此,实用的解决方案可能是使用三个解决方案中的任何一个,使用更少的空间和时间:对于 k 的值足够大,n 位位向量小于具有 k 个条目的哈希表,但没有大到拒绝的概率很大(例如,n/64≤k ≤n/4),使用位向量。对于较小的 k 值,使用简单的哈希表算法,对于接近 n 的 k 值,使用 Fisher-Yates shuffle完整的 n 元素向量(但仅限于 k 步)。
因为我们只在 k>cn 的情况下选择 O(n) 策略常数 c,复合算法仍然是 O(k) 时间和空间,因为在该约束下,n 在 O(k )。