《Programming Pearls 2th edition》第12章当中的"取样问题"非常有趣,现在摘录一些下来
洗牌算法,随机打乱一个0、1、2、...、n-1序列当中的数字
import random
def shuffle_1(x):
for i in range(len(x)-1):
j = random.randint(i, len(x)-1)
x[i], x[j] = x[j], x[i]
这个算法可以理解为:n个人以1/n的概率竞选第1个位置,剩下的n-1个人以1/(n-1)概率获得第2个位置,……最后2个人以1/2机会获得第n-1位置。
保证了生成大小为n的全排列中每一个排列的机会是相等的。
取样问题:
p1 从n个数当中随机取m个数,每个数被选中的概率均等
特殊化,考虑m为1的情况
p2 从n个数当中随机选取m个数,每个大小为m的子集选中的概率均等
p3 从n个数当中随机选取m个数,每个大小为m的排列选中的概率均等
p4 从一列数字当中选取m个数,但是不知道数列的大小,每个数被选中的概率均等
特殊化 ,考虑 m为1的情况
解答:
p1
def sample_1(x, m):
n = len(x)
if m >= n:
return list(x)
ret = []
u = m
w = n
for i in range(n):
if w == 0:
break
if random.randint(1, w) <= u:
u -= 1
ret.append(i)
w -= 1
return ret
u代表需要选取的数量,w代表列表中的备选数量。对于剩下来没有选中的数,以u/w的概率依次选取。因此每个数被选取的概率在剩下的数当中都是平等的。例如u=2, w=4,接下来的数有2/4的概率被选取。一旦被选取,接下来的一个数有1/3的概率获选;否则,有2/3概率。每一个局部的公平保持了整体的公平。p2等价于p1
p3
def sample_3(x, m):
for i in range(m):
j = random.randint(i, len(x)-1)
x[i], x[j] = x[j], x[i]
ret = x[:m]
return ret
这个算法可以理解为:n个人中找一个人以1/n机会获得第1个位置;剩下的n-1个人再找一个人以1/(n-1)的机会获取第2个位置;依此类推,直到前m个位置都有人选。此算法和洗牌算法的原理一致,只是洗牌算法中m等于n。
这个算法保证了从n个数中随机选取m个人,且m个人的排列是随机的
p4
def sample_4(x, m):
ret = []
for i in range(len(x)):
if len(ret) < m:
ret.append(x[i])
elif len(ret) == m:
if random.randint(0, i) <= m:
j = random.randint(0, m-1)
ret[j] = x[i]
return ret
x中前m个数先放到位置上。接下来的第i个数,以m/i的概率被选中。从已选的m个数当中选择一个数换出来。
这个算法保证了每个数都有相同的概率被选中。
从第m+1个数开始,选中后所处的位置也是随机的。但是前m个数如果中途没有被换出来,它们的位置不是随机的。
因此,此算法选出的数的排列不是随机的。需要开始对前m个数随机排序,才能得到随机的排序。
进一步阅读
《the Art of Computer Programming》第2卷第3章“随机算法”