洗牌算法

洗牌算法

    • 导语
    • 抽牌 - Fisher-Yates Shuffle
    • 换牌 - Knuth-Durstenfeld Shuffle
    • 插牌 - Inside-Out Algorithm
    • 扩展 - 蓄水池抽样

导语

在之前的【Leetcode 384】Shuffle an Array - MEDIUM,打乱一个数组的问题中,我们有用到洗牌算法,其关键在于生成等概率的结果。

而联系实际的洗牌过程,都有哪几种方式呢?

  1. 每次随机抽出一张牌 放到一边(不放回),这是抽牌
  2. 每次随机抽出一张未处理的牌与当前最后一张牌交换,然后将当前最后一张牌放到一边(不放回),这是换牌
  3. 每次随机抽出前 i 张(含第 i 张)中任意一张与第 i 张交换,相当于第 i 张插入到前 i 张中,这是插牌

抽牌 - Fisher-Yates Shuffle

每次随机抽出一张牌 放到一边(不放回)。

随机性:
元素m被放入第i个位置的概率P = 前i-1个位置选择元素时没有选中m的概率 * 第i个位置选中m的概率。
在这里插入图片描述
算法实现:

    public int[] shuffle() {
    	Random random = new Random();
    	LinkedList <Integer> temp = new LinkedList<>(list);
    	int index;
    	for(int i=len-1;i>=0;i--){
    		index = random.nextInt(i+1);
    		shuffledNums[i] = initNums[temp.get(index)];
    		temp.remove(index);
    	}
        return shuffledNums;
    }

时间复杂度:O(n)
空间复杂度:O(n)

换牌 - Knuth-Durstenfeld Shuffle

每次随机抽出一张未处理的牌与当前最后一张牌交换,然后将当前最后一张牌放到一边(不放回)。

随机性:
对于任意元素,洗牌后在倒数第 1 个的位置的概率为 1/n(第一次即被选中),倒数第 2 个的位置的概率为[(n-1) / n][ 1 / (n-1)](第一次未被选中,第二次被选中),倒数第 i 个的位置的概率是[(n-1) / n] * [(n-2) / (n-1)] * … * [(n - i + 1) / (n - i + 2)] *[1 / (n - i + 1)] = 1/n。

即任意元素在任意位置的概率均为1 / n。

    public int[] shuffle() {
    	Random random = new Random();
    	int index;
    	for(int i=len-1;i>=0;i--){
    		index = random.nextInt(i+1);
    		swap(shuffledNums, index, i);
    	}
        return shuffledNums;
    }

时间复杂度:O(n)
空间复杂度:O(1)

换牌算法与抽牌算法类似,只是没有开辟新的内存空间,而是在原数组内打乱。如果需要保存原序列,仍然需要开辟一个新数组保存下来。
另外,由于它是由后往前遍历的,所以只能针对长度固定且已知的数组打乱。

插牌 - Inside-Out Algorithm

每次随机抽出前 i 张(含第 i 张)中任意一张与第 i 张交换。

随机性:
对于新数组中第 i 个元素,一定是第 i 次被换到了该位置,所以前i - 1次交换的结果与它无关,而它第 i 次被交换至此的概率为1/i,剩余次数内不被暗中的概率为 [ i / (i+1)] * [(i+1) / (i+2)] * [(i+2) / (i+3)]* … * [(n-1) / n ],

所以任意元素在第 i 个位置的概率为1 / n。

算法实现:

    public int[] shuffle() {
    	Random random = new Random();
    	int index;
    	for(int i=0;i<len;i++){
    		index = random.nextInt(i+1);
    		swap(shuffledNums, index, i);
    	}
        return shuffledNums;
    }

时间复杂度:O(n)
空间复杂度:O(1)

可以应对长度未知或者长度动态增加的数组打乱问题。

扩展 - 蓄水池抽样

蓄水池是这样一个问题:
给定一个数据流,数据流长度N很大,且N直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出m个不重复的数据。

我们可以想象有一个蓄水池,里面包含m个初始元素。
从第k个(m + 1 < = k < = n)开始遍历,从前m+1个元素中随机选取一个元素,如果该元素在蓄水池中,则将其换成当前元素,否则do nothing。

    public int[] ReservoirSampling(int[] dataStream, int m) {
    	Random random = new Random();
    	int[] reservoir = Arrays.copyOf(dataStream, m);
    	int index;
    	for(int i=m; i<dataStream.length;i++){
			index = random.nextInt(i+1);
			if(index < m)
				reservoir[index] = dataStream[i];
		}
        return reservoir;
    }

时间复杂度:O(n)
空间复杂度:O(m)

你可能感兴趣的:(算法)