【leetcode】384 打乱数组(洗牌算法)

题目链接:https://leetcode-cn.com/problems/shuffle-an-array/

题目描述

打乱一个没有重复元素的数组。

示例:

// 以数字集合 1, 2 和 3 初始化数组。
int[] nums = {1,2,3};
Solution solution = new Solution(nums);

// 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。
solution.shuffle();

// 重设数组到它的初始状态[1,2,3]。
solution.reset();

// 随机返回数组[1,2,3]打乱后的结果。
solution.shuffle();

洗牌算法

n个不同的数中随机取出不重复的m个数

1. Fisher–Yates shuffle

基本思想是从原始数组中随机取一个之前没取过的数字到新的数组中,具体如下:
(1)初始化原始数组和新数组,原始数组长度为n(已知)
(2)从还没出来IDE数组(假设还剩k个)中,随机产生一个[0,k)
(3)从剩下的k个数中把第p个数取出;
(4) 重复步骤2和3直到数字全部取完;
(5)从步骤3取出的数字序列便是一个打乱了的数列。

下面证明其随机性,即每个元素被放置在新数组中的第i个位置是1/n(假设数组大小是n)。
证明:一个元素m被放入第i个位置的概率P = 前i-1个位置选择元素时没有选中m的概率 * 第i个位置选中m的概率,即
P = n 1 n × n 2 n 1 × × n i + 1 n i + 2 × 1 n i + 1 = 1 n P=\frac{n-1}{n}\times\frac{n-2}{n-1}\times\dots\times\frac{n-i+1}{n-i+2}\times\frac{1}{n-i+1}\\=\frac{1}{n}

#define N 10
#define M 5
void Fisher_Yates_Shuffle(vector<int>& arr,vector<int>& res)
{
     srand((unsigned)time(NULL));
     int k;
     for (int i=0;i<M;++i)
     {
     	k=rand()%arr.size();
     	res.push_back(arr[k]);
     	arr.erase(arr.begin()+k);
     }
}

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

2. Knuth-Durstenfeld Shuffle

Knuth 和 Durstenfeld 在Fisher 等人的基础上对算法进行了改进,在原始数组上对数字进行交互,省去了额外O(n)的空间。该算法的基本思想和 Fisher 类似,每次从未处理的数据中随机取出一个数字,然后把该数字放在数组的尾部,即数组尾部存放的是已经处理过的数字。

算法步骤为:
(1) 建立一个数组大小为 n 的数组 arr,分别存放 1 到 n 的数值;
(2)生成一个从 0 到 n - 1 的随机数 x;
(3)输出 arr 下标为 x 的数值,即为第一个随机数;
(4)将 arr 的尾元素和下标为 x 的元素互换;
(5)同2,生成一个从 0 到 n - 2 的随机数 x;
(6)输出 arr 下标为 x 的数值,为第二个随机数;
(7)将 arr 的倒数第二个元素和下标为 x 的元素互换;
如上,直到输出 m 个数为止

时间复杂度为O(n),空间复杂度为O(1),缺点必须知道数组长度n.

void Knuth_Durstenfeld_Shuffle(vector<int>&arr)
{
	for (int i=arr.size()-1;i>=0;--i)
	{
		srand((unsigned)time(NULL));
		swap(arr[rand()%(i+1)],arr[i]);
	}

原始数组被修改了,这是一个原地打乱顺序的算法,算法时间复杂度也从Fisher算法的 O(n2)提升到了O(n)。由于是从后往前扫描,无法处理不知道长度或动态增长的数组。

3. Inside-Out Algorithm

Knuth-Durstenfeld Shuffle 是一个内部打乱的算法,算法完成后原始数据被直接打乱,尽管这个方法可以节省空间,但在有些应用中可能需要保留原始数据,所以需要另外开辟一个数组来存储生成的新序列。

Inside-Out Algorithm 算法的基本思想:
从前向后扫描数据,把位置i的数据随机插入到前i个(包括第i个)位置中(假设为k),这个操作是在新数组中进行,然后把原始数据中位置k的数字替换新数组位置i的数字。其实效果相当于新数组中位置k和位置i的数字进行交互。

如果知道arr的lengh的话,可以改为for循环,由于是从前往后遍历,所以可以应对arr[]数目未知的情况,或者arr[]是一个动态增加的情况

证明如下:
原数组第i个元素(随机到的数)在新数组的前i个位置的概率都是:
1 i × i i + 1 × i + 1 i + 2 × n 1 n = 1 n \frac{1}{i} \times \frac{i}{i+1} \times \frac{i+1}{i+2} \dots \times \frac{n-1}{n} = \frac{1}{n}
(即第i次刚好随机放到了该位置,在后面的n-i 次选择中该数字不被选中)

原数组的第 i 个元素(随机到的数)在新数组的 i+1 (包括i + 1)以后的位置(假设是第k个位置)的概率是:
1 k × k k + 1 × n 1 n = 1 n \frac{1}{k} \times \frac{k}{k+1} \dots \times \frac{n-1}{n} = \frac{1}{n}
(即第k次刚好随机放到了该位置,在后面的n-k次选择中该数字不被选中)。

void Inside_Out_Shuffle(const vector<int>&arr,vector<int>& res)
{
	res.assign(arr.size(),0);
	copy(arr.begin(),arr.end(),res.begin());
	int k;
	for (int i=0;i<arr.size();++i)
	{
		srand((unsigned)time(NULL));
		k=rand()%(i+1);
		res[i]=res[k];
		res[k]=arr[i];
	}
} 

4. 蓄水池抽样

从N个元素中随机等概率取出k个元素,N长度未知。它能够在o(n)时间内对n个数据进行等概率随机抽取。如果数据集合的量特别大或者还在增长(相当于未知数据集合总量),该算法依然可以等概率抽样。
先选中第1到k个元素,作为被选中的元素。然后依次对第k+1至第N个元素做如下操作:
每个元素都有k/x的概率被选中,然后等概率的(1/k)替换掉被选中的元素。其中x是元素的序号。
证明:第m个对象被选中的概率=选择m的概率 * (其后元素不被选择的概率+其后元素被选择的概率*不替换第m个对象的概率),即
在这里插入图片描述

void Reservoir_Sampling(vector<int>& arr)
{
	int k;
	for (int i=M;i<arr.size();++i)
	{
		srand((unsigned)time(NULL));
		k=rand()%(i+1);
		if (k<M)
			swap(arr[k],arr[i]); 
	}
}

代码

/*
 * Knuth-Durstenfeld Shuffle
 * 随机产生[0,k]位置随机数,与位置k元素交换;每次产生一个随机数放队列末尾
 * 原地打乱算法 时间复杂度O(n)
 */
class Solution {
    vector<int> origin;
public:
    Solution(vector<int>& nums) {
        origin = nums;
    }

    /** Resets the array to its original configuration and return it. */
    vector<int> reset() {
        return origin;
    }

    /** Returns a random shuffling of the array. */
    vector<int> shuffle() {
        vector<int> arr(origin);
        for (int i = arr.size() -1; i >=0 ; --i) {
            swap(arr[rand()%(i+1)], arr[i]);
        }
        return arr;
    }
};
/*
 * Inside-Out Algorithm
 * 从前向后扫描数据,把位置i的数据随机插入到前i个(包括第i个)位置中(假设为k)
 * 时间复杂度O(n) 空间复杂度O(n) 
 */
class Solution {
    vector<int> origin;
public:
    Solution(vector<int>& nums) {
        origin = nums;
    }

    /** Resets the array to its original configuration and return it. */
    vector<int> reset() {
        return origin;
    }

    /** Returns a random shuffling of the array. */
    vector<int> shuffle() {
        vector<int> arr(origin);
        for (int i = 0; i <arr.size() ; ++i) {
            swap(arr[rand()%(i+1)], arr[i]);
        }
        return arr;
    }
};

猜你喜欢

转载自blog.csdn.net/zjwreal/article/details/89604275