简易编码O(N + K * log(K))方式
在不替换索引的情况下抽取随机样本,对索引进行排序,然后从原始索引中获取。
indices = random.sample(range(len(myList)), K)
[myList[i] for i in sorted(indices)]
或更简而言之:
[x[1] for x in sorted(random.sample(enumerate(myList),K))]
优化的O(N)时间,O(1)-辅助空间方式
您也可以使用数学技巧,myList
从左到右迭代,以动态变化的概率选择数字(N-numbersPicked)/(total-numbersVisited)
。这种方法的优点是它是一种O(N)
算法,因为它不涉及排序!
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
概念验证和概率证明:
在5小时内用1万亿个伪随机样本进行了仿真:
>>> Counter(
tuple(orderedSampleWithoutReplacement([0,1,2,3], 2))
for _ in range(10**9)
)
Counter({
(0, 3): 166680161,
(1, 2): 166672608,
(0, 2): 166669915,
(2, 3): 166667390,
(1, 3): 166660630,
(0, 1): 166649296
})
概率与真实概率的相差小于1.0001。再次运行此测试会导致顺序不同,这意味着它不会偏向一个顺序。运行与较少的样本测试[0,1,2,3,4], k=3
和[0,1,2,3,4,5], k=4
有类似的结果。
编辑:不确定为什么人们投票反对错误的评论或害怕投票...不,这种方法没有错。=)
(这也是tegan用户在注释中的有用注释:如果这是python2,则如果您确实关心额外的空间,则将像往常一样使用xrange。)
编辑:证明:考虑选择的一个子集的均匀分布(无需更换)k
从人口seq
规模len(seq)
,我们可以在任意点考虑划分i
成“左”(0,1,...,I-1)和'right'(i,i + 1,...,len(seq))。考虑到我们是numbersPicked
从左边的已知子集中选取的,其余的必须来自右边的未知子集中的相同均匀分布,尽管参数现在不同。特别是,seq[i]
包含选定元素的概率为#remainingToChoose/#remainingToChooseFrom
,或(k-numbersPicked)/(len(seq)-i)
,因此我们对此进行了仿真并根据结果进行递归。(这必须终止,因为如果#remainingToChoose == #remainingToChooseFrom,则所有剩余概率均为1。)这类似于动态生成的概率树。基本上,您可以通过以先验选择为条件来模拟均匀的概率分布(随着概率树的生长,您选择当前分支的概率,使其与先验叶子相同,即以先验选择为条件;这将起作用,因为该概率统一精确地为N / k)。
编辑:蒂莫西·希尔兹(Timothy Shields)提到了“水库采样”,这是在len(seq)
未知(例如带有生成器表达式)时此方法的推广。具体来说,如果在原位完成,则称为“算法R”的是O(N)和O(1)空间。它涉及获取第一个N元素并慢慢替换它们(也给出了归纳证明的提示)。在Wikipedia页面上还可以找到有用的分布式采样和其他类型的储层采样。
编辑:这是下面一种在语义上更明显的方式对其进行编码的方法。
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)
)
random.sample
然后排序?