蓄水池抽样算法
蓄水池抽样算法随机算法的一种,用来从 N 个样本中随机选择 K 个样本,其中 N 非常大(以至于 N 个样本不能同时放入内存)或者 N 是一个未知数。其时间复杂度为 O(N),包含下列步骤 (假设有一维数组 S, 长度未知,需要从中随机选择 k 个元素, 数组下标从 1 开始), 伪代码如下:
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
理解:算法首先创建一个长度为 k 的数组(蓄水池)用来存放结果,初始化为 S 的前 k 个元素。然后从 k+1 个元素开始迭代直到数组结束,在 S 的第 i 个元素,算法生成一个随机数 j∈[1,i], 如果 j <= k, 那么蓄水池的第 j 个元素被替换为 S 的第 i 个元素。
算法的正确性证明:
定理:该算法保证每个元素以 k / n 的概率被选入蓄水池数组。
证明:
当i=k+1时,第k+1个元素被选中的概率是
kk+1
,而前k个元素被选中的概率=1 - 被第k+1个元素替换的概率 =
1−kk+1×1k=kk+1
,说明前面k+1个元素被取到的概率都是相等的且均为
kk+1
。
下面采用数学归纳法进行证明。
假设i=n时,前所有的元素都以
kn
被选中。
那么当i=n+1是,第n+1个元素被选中的概率为
kn+1
对于前面的n个元素,每个元素被选中的情况分为两种:1.前面n次已经被选中并且第n+1次时,第n+1个元素没有被选中;2.前面n此已经被选中,第n+1个元素被选中但是没有将其替换掉。不难写出此时的概率为:
kn×(1−kn+1)+kn×(kn+1×(1−1k))=kn+1
由此可见,第n+1步也满足假设条件。所以问题得到证明。
随机洗牌算法
一般的洗牌算法
1、利用一个队列
2、每次从数组中,随机找到一个数;
若该数没有被选择过,那么就将它放入队列中;如果被选择过,就重新随机
毋庸置疑,这个算法是可以满足洗牌的要求的。然而呢,让我们看一下该算法时间复杂度。每次选择一个数,可能存在已经选择过的情况。所以该算法的时间复杂度是O(N* N),并且又多用到了一个队列,空间复杂度也不是很好
更优的洗牌算法
该算法类似于插入排序
从数组的最后一个数(下标为i)开始,进行随机取余(除数为i+1,确保下标不过界)将得到的下标对应的元素和最后一个数交换将最后一个数不在视为数组中的元素,继续循环直到只剩下第一个元素为止。代码如下:
//进行洗牌
void Shuffe(int* a, int n)
{
for (int i = n - 1; i > 0; --i)
{
srand((unsigned)time(NULL));//不加此语句的话,每次随机的数会不变
swap(a[i], a[rand() % (i + 1)]);
}
}
证明:
假设总共有n个元素(1~n)顺序排列,示意图如下:
。随机打乱的意思就是每个元素出现在任意位置的概率为
1n
。假设其中第k个元素,随机到0~k中第p的位置上,计算概率。要是k换位置到p位置上,要满足首先不能被换到k~n的任意位置,其次在k位置上换到p位置上,则概率为:
P=n−1n×n−2n−1×⋯×kk+1×1k=1n
要是k位置上的元素被换到k~n的第q位置上的概率,要满足首先在q~n不被换,同时在q位置上被换,公式和上面的一样。因此出现在任意位置上的概率是 1n 。类似于蓄水池抽样算法。