简单的代码 O(#picks*log(#picks)) 方式
在不替换索引的情况下随机抽取样本,对索引进行排序,然后从原始样本中获取。
indices = random.sample(range(len(myList)), K)
[myList[i] for i in sorted(indices)]
random.sample(seq, K) 将从seq 的总体中随机同时选择 K 个元素,无需替换。当我们使用range 执行此操作时,每个样本都是 O(1),因为 python 中的 range 对象是稀疏的,实际上并没有构造一个完整的列表(特别是 cpython 实现调用len(seq) 和后来的seq[i]位于范围对象上,这些对象是虚拟化/伪造的,因此是 O(1))。然后查找随机索引(按顺序)。
如果您有迭代器(例如生成器表达式),您可以考虑先转换为列表,然后执行上述答案。如果您的迭代器是无限长度的,您可以使用下一节中的技术,它的性能要低得多,但可能在智力上很有趣(例如,如果您正在使用尚不支持索引的函数式语言中的小型有界列表,或超过 RAM 和磁盘大小的巨型流):
(也是来自 cmets 中用户 tegan 的有用说明:如果这是 python2,您将像往常一样使用 xrange。否则,您将使用 O(N) 而不是 O(#picks) 算法。)
用于不可索引序列的慢速/可流式 O(arrayLen)-时间、O(1)-辅助空间方式
您也可以使用数学技巧,从左到右迭代地遍历myList,选择具有动态变化概率(N-numbersPicked)/(total-numbersVisited) 的数字。这种方法是O(N),因为它访问所有内容一次(比排序更快,即 O(N log(N)),但比我们在上一节中直接索引 K 选择要慢得多(即 O(K log( K)) 排序后)。
from __future__ import division
def orderedSampleWithoutReplacement(seq, k):
if not 0<=k<=len(seq):
raise ValueError('Required that 0 <= sample_size <= population_size')
numbersPicked = 0
for i,number in enumerate(seq):
prob = (k-numbersPicked)/(len(seq)-i)
if random.random() < prob:
yield number
numbersPicked += 1
证明:考虑到从大小为len(seq) 的总体seq 中挑选k 的子集的均匀分布(无替换),我们可以考虑将任意点i 的分区划分为“左” (0,1,...,i-1) 和“右”(i,i+1,...,len(seq))。鉴于我们从左侧已知子集中选择numbersPicked,剩余的必须来自右侧未知子集的相同均匀分布,尽管参数现在不同。特别是,seq[i] 包含所选元素的概率为#remainingToChoose/#remainingToChooseFrom 或(k-numbersPicked)/(len(seq)-i),因此我们对其进行模拟并对结果进行递归。 (这必须终止,因为如果 #remainingToChoose == #remainingToChooseFrom,则所有剩余概率均为 1。)这类似于碰巧动态生成的概率树。基本上,您可以通过以先验选择为条件来模拟均匀概率分布(当您生长概率树时,您选择当前分支的概率,使其与先验叶子后验相同,即以先验选择为条件;这将起作用,因为这个概率一致地恰好是 N/k)。
(人们可能会查看这篇文章的编辑历史,以找到一个精心制作的模拟“证明”,这在以前是必要的,因为一些反对意见。)
下面是另一种编码方式,具有更多语义命名的变量。
from __future__ import division
import random
def orderedSampleWithoutReplacement(seq, sampleSize):
totalElems = len(seq)
if not 0<=sampleSize<=totalElems:
raise ValueError('Required that 0 <= sample_size <= population_size')
picksRemaining = sampleSize
for elemsSeen,element in enumerate(seq):
elemsRemaining = totalElems - elemsSeen
prob = picksRemaining/elemsRemaining
if random.random() < prob:
yield element
picksRemaining -= 1
from collections import Counter
Counter(
tuple(orderedSampleWithoutReplacement([0,1,2,3], 2))
for _ in range(10**5)
)
edit:Timothy Shields 提到了Reservoir Sampling,这有点像这种方法(但从候选选择开始并随机交换它们),并且在len(seq) 未知时很有用(例如带有生成器表达式)。具体来说,被称为“算法 R”的那个是 O(N) 和 O(1) 辅助空间,如果就地完成的话;它涉及获取前 K 个元素并慢慢替换它们(还给出了归纳证明的提示)。在维基百科页面上还可以找到有用的水库采样变体。这个想法是你预先填充一个候选返回值列表(我们假设它适合在 RAM 或磁盘上),并在你遍历列表时概率性地将它们交换出来(它可能比你的 RAM 或磁盘任意大)。
可索引序列的最佳 O(#picks * (1+log(N/#picks)))-time, O(1)-aux-space 方式
一个值得注意的算法在 Reservoir Sampling 文章中(ctrl-F Algorithm L section“最优算法”):它是一种竞争因素最优算法,(与原始解决方案一样)数量为 O(k)样本数量,而不是列表元素数量的 O(n)。
这里的直觉是我们可以跳过列表的任意部分,甚至不必访问它们,因为选择之间的元素数量不依赖于我们在列表中看到的数据。
与依赖于上述超几何分布不同,水库预先填充了候选解决方案(前 k 个项目)并定期换出这一事实,这使其看起来更像是一个具有几何等待时间的过程.这是一篇被广泛引用的论文,但我无法通过它来判断证明对于大 N 是渐近正确的,还是对所有 N 都有效。
从文章中不清楚是否可以在开始时不知道序列长度的情况下使用此算法(在这种情况下,可能只使用此答案第一部分中的原始方法)。