【随机算法】洗牌

$0 384. 打乱数组

直接使用 std::shuffle 的写法

class Solution {
public:
    Solution(vector<int>& nums) {
        vec = nums;
    }

    vector<int> reset() {
        return vec;
    }

    vector<int> shuffle() {
        vector<int> result = vec;
        int seed = rand();
        std::default_random_engine dre(seed);
        std::shuffle(result.begin(), result.end(), dre);
        return result;
    }

private:
    vector<int> vec;
};

$1 洗牌算法

(1) 基于抽取 Fisher-Yates Shuffle

基本思想就是从原始数组中随机取一个之前没取过的数字到新的数组中

初始化 vec, new_vec,长度为 n
每一轮从 vec 中取出一个,放到 new_vec 中
当前轮次,vec 的长度为 k
    取 0 <= random_idx < k
    从 vec 中将 k 取出,放入 new_vec
知道 vec 耗尽

正确性证明

一个元素 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 ... \times \frac{n-i+1}{n-i+2} \times \frac{1}{n-i+1} = \frac{1}{n} p=nn1×n1n2×...×ni+2ni+1×ni+11=n1

代码

时间 O ( N 2 ) O(N^{2}) O(N2),空间 O ( N ) O(N) O(N)

class Solution {
public:
    Solution(vector<int>& nums) {
        vec = nums;
        int seed = rand();
        dre = std::default_random_engine(seed);
        dr = std::uniform_real_distribution<double>(0.0, 1.0);
    }

    vector<int> reset() {
        return vec;
    }

    vector<int> shuffle() {
        vector<int> tmp(vec.begin(), vec.end());
        vector<int> result;
        int n = tmp.size();
        for(int i = 0; i < n; ++i)
        {
            int k = tmp.size();
            int random_idx = floor(dr(dre) * k);
            result.push_back(tmp[random_idx]);
            tmp.erase(tmp.begin() + random_idx);
        }
        return result;
    }

private:
    vector<int> vec;
    std::default_random_engine dre;
    std::uniform_real_distribution<double> dr;
};

(2) 基于交换 Knuth-Durstenfeld Shuffle

对基于抽取的 Fisher-Yates Shuffle 的优化。每次抽取出的牌直接交换到原数组尾部,而不是从原数组删掉再插入到新数组。

代码

时间 O ( N ) O(N) O(N),空间 O ( N ) O(N) O(N)

class Solution {
public:
    Solution(vector<int>& nums) {
        vec = nums;
        int seed = rand();
        dre = std::default_random_engine(seed);
        dr = std::uniform_real_distribution<double>(0.0, 1.0);
    }

    vector<int> reset() {
        return vec;
    }

    vector<int> shuffle() {
        vector<int> result(vec.begin(), vec.end());
        int n = result.size();
        for(int k = n; k >= 1; --k)
        {
            int random_idx = floor(dr(dre) * k);
            swap(result[random_idx], result[k - 1]);
        }
        return result;
    }

private:
    vector<int> vec;
    std::default_random_engine dre;
    std::uniform_real_distribution<double> dr;
};

(3) 基于插入 Inside-Out Algorithm

Inside-Out Algorithm 算法的基本思思是从前向后扫描数据,把位置 i 的数据随机插入到前 i 个位置中:

插入位置确定:当前是原数组第 i 个位置(vec[i]),取 [0, i] 范围的随机下标 random_idx = floor(dr(dre) * (i + 1))
vec[i] 放进新数组的 random_idx ,但该位置可能已经插入了前面的值。此时将原有的值交换到新数组的 i 位置(此位置当前肯定没有插入过元素)。

其实效果相当于新数组中位置 k 和位置 i 的数字进行交互。

正确性证明

对于 nums[i],在新数组中的位置为 j。

  • 0 <= j <= i

第 i 次刚好随机放到了 j 位置,在后面的 n - i 次选择中该数字不被选中

p ( j ) = 1 i × i i + 1 × i + 1 i + 2 × . . . × n − 1 n = 1 n p(j) = \frac{1}{i} \times \frac{i}{i+1} \times \frac{i+1}{i+2} \times ... \times \frac{n-1}{n} = \frac{1}{n} p(j)=i1×i+1i×i+2i+1×...×nn1=n1

  • i + 1 <= j < n

假设是第 k 次 (i+1 <= k < n) 随机到了 j 位置。在后面的 n-k 次选择中该数字不被选中。

p ( j ) = 1 k × k k + 1 × k + 1 k + 2 × . . . × n − 1 n = 1 n p(j) = \frac{1}{k} \times \frac{k}{k+1} \times \frac{k+1}{k+2} \times ... \times \frac{n-1}{n} = \frac{1}{n} p(j)=k1×k+1k×k+2k+1×...×nn1=n1

代码

时间 O ( N ) O(N) O(N),空间 O ( N ) O(N) O(N)

class Solution {
public:
    Solution(vector<int>& nums) {
        vec = nums;
        int seed = rand();
        dre = std::default_random_engine(seed);
        dr = std::uniform_real_distribution<double>(0.0, 1.0);
    }

    vector<int> reset() {
        return vec;
    }

    vector<int> shuffle() {
        int n = vec.size();
        vector<int> result(n);
        for(int i = 0; i < n; ++i)
        {
            int random_idx = floor(dr(dre) * (i + 1));
            swap(result[random_idx], result[i]);
            result[random_idx] = vec[i];
        }
        return result;
    }

private:
    vector<int> vec;
    std::default_random_engine dre;
    std::uniform_real_distribution<double> dr;
};

$2 std::shuffle 的实现

算法

Ref: 《计算机程序设计艺术》 $3-4-2

void shuffle(RandomAccessIterator first, RandomAccessIterator last, RandomNumberGenerator& rand)
{
    if(first == last) return;
    for(auto i = first + 1; i != last; ++i)
        iter_swap(i, first + rand((i - first) + 1));
}

代码测试

class Solution {
public:
    Solution(vector<int>& nums) {
        vec = nums;
        int seed = rand();
        dre = std::default_random_engine(seed);
        dr = std::uniform_real_distribution<double>(0.0, 1.0);
    }

    vector<int> reset() {
        return vec;
    }

    vector<int> shuffle() {
        vector<int> result = vec;
        shuffle(result.begin(), result.end(), dre);
        return result;
    }

private:
    void shuffle(vector<int>::iterator first, vector<int>::iterator last, std::default_random_engine& rand)
    {
        if(first == last) return;
        std::uniform_real_distribution<double> dr(0.0, 1.0);
        for(auto i = first + 1; i != last; ++i)
            iter_swap(i, first + floor(dr(rand) * (i - first + 1)));
    }

    vector<int> vec;
    std::default_random_engine dre;
    std::uniform_real_distribution<double> dr;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

算法题刷刷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值