原题是这样的:
假设有100G的日志存在于磁盘之上,其中每条日志占用的空间不多于100个字节,现在从日志随机选取N条日志,保证每条日志的选取概率相同。
解1:
最简单且严格的做法是扫描日志两遍。
第一遍:统计出日志的总条数,M。
第二遍:扫描每条日志以N/M的概率选择该条日志,直到满足N条日志。
解法2:
由于每条日志的最大长度为100字节,因此日志数量至少有K = 1G条,因此可以在扫描日志的时候进行随机抽样,使用一个较大的概率比如 10 * N / K。 这样选择出的日志条数肯定会大于N,并且总量远小于100G。 接下来可以按第一种方法从样本中随机选择出N条日志。
PS: 解法1和2都比较简单,但是会存在一个非常严重的问题,都需要读取所有的日志,100G的数据在读取时会非常耗时,最理想情况下以厂商的标准最快可以达到80M/s,这样读取100G的数据同样需要1250s,即20多分钟。那有没有一种方法减少读取磁盘的数据量?
解法3:假设文件的总大小为S=100G,在逻辑上将文本分成B=100W块,则每个块的实际大小为K=S/B, 然后在逻辑上遍历这100W个块,给每个块1/1000的选择概率。这样按偏移量从中选取1000个块,在这1000个块中随机抽取N条日志。这样实际读取的数据量大小为100M, 这已经非常小。
注:第三种方法可以会在每个日志的选取概率上出现细微的区别,但是由于数据量巨大,故这种差别可以忽略。
从1楼的评论中学习到一个算法 -- 蓄水池抽样,原理如下:
定义取出的行号为choice,第一次直接以第一行作为取出行 choice ,而后第二次以二分之一概率决定是否用第二行替换 choice ,第三次以三分之一的概率决定是否以第三行替换 choice ……,以此类推,可用伪代码描述如下:
i = 0
while more input lines
with probability 1.0/++i
choice = this input line
print choice
这种方法的巧妙之处在于成功的构造出了一种方式使得最后可以证明对每一行的取出概率都为1/n(其中n为当前扫描到的文件行数),换句话说对每一行取出的概率均相等,也即完成了随机的选取。
回顾这个问题,我们可以对其进行扩展,即如何从未知或者很大样本空间随机地取k个数?
类比下即可得到答案,即先把前k个数放入蓄水池,对第k+1,我们以k/(k+1)概率决定是否要把它换入蓄水池,换入时随机的选取一个作为替换项,这样一直做下去,对于任意的样本空间n,对每个数的选取概率都为k/n。也就是说对每个数选取概率相等。
伪代码:
Init : a reservoir with the size: k
for i= k+1 to N
M=random(1, i);
if( M < k)
SWAP the Mth value and ith value
end for