洗牌算法

释放双眼,带上耳机,听听看~!

洗牌算法

  • 导语
    • 抽牌 – 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的概率。
洗牌算法
算法实现:


1
2
3
4
5
6
7
8
9
10
11
12
13
1    public int[] shuffle() {
2       Random random = new Random();
3       LinkedList <Integer> temp = new LinkedList<>(list);
4       int index;
5       for(int i=len-1;i>=0;i--){
6           index = random.nextInt(i+1);
7           shuffledNums[i] = initNums[temp.get(index)];
8           temp.remove(index);
9       }
10        return shuffledNums;
11    }
12
13

时间复杂度: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。


1
2
3
4
5
6
7
8
9
10
11
1    public int[] shuffle() {
2       Random random = new Random();
3       int index;
4       for(int i=len-1;i>=0;i--){
5           index = random.nextInt(i+1);
6           swap(shuffledNums, index, i);
7       }
8        return shuffledNums;
9    }
10
11

时间复杂度: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。

算法实现:


1
2
3
4
5
6
7
8
9
10
11
1    public int[] shuffle() {
2       Random random = new Random();
3       int index;
4       for(int i=0;i<len;i++){
5           index = random.nextInt(i+1);
6           swap(shuffledNums, index, i);
7       }
8        return shuffledNums;
9    }
10
11

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

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

扩展 – 蓄水池抽样

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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
1    public int[] ReservoirSampling(int[] dataStream, int m) {
2       Random random = new Random();
3       int[] reservoir = Arrays.copyOf(dataStream, m);
4       int index;
5       for(int i=m; i&lt;dataStream.length;i++){
6           index = random.nextInt(i+1);
7           if(index &lt; m)
8               reservoir[index] = dataStream[i];
9       }
10        return reservoir;
11    }
12
13

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

给TA打赏
共{{data.count}}人
人已打赏
安全经验

独立博客怎样申请谷歌Adsense

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索