【洗牌算法】C++将数组的元素顺序随机打乱(条件概率证明算法充分随机)

将数组顺序打乱

做模拟需要用到将一个数组内的元素随机打乱的需求,需要一个‘洗牌算法’,也就是需要生成数组下标的一个随机顺序

实现的思路如下(思路一比较复杂不好理解,推荐思路二):

以将一个元素个数为10的数组打乱为例:

思路 1


开始先循环一次生成0-9之间的一个数作为第一个下标,此时原数组的位置已经被占用了一个(实际第一次生成的随机下标就是最终的下标了,因为之前位置没有被占用);

然后生成一个0-8之间的数作为第二个下标,但这个下标是对应于剩下空间所在的数组的下标,而不是原数组的下标。比如上面第一次生成的下标为6,这里生成的还是6的话,那这里的下标在原数组中应该是7,下标6所在位置已经被占用了。同理如果这里生成的下标是7,那在原数组中对应应为8。但如果这里生成的下标为5,比之前的小,那在原数组中对应下标还是5.问题的难度就在于将生成的下标还原到原数组中,不能出现下标重叠的情况;

之后依次生成0-7,0-6,… ,最后一个确定是0的随机下标。

将得到的10个随机下标还原回原数组中主要是根据随机生成所在的数组和原数组的关系进行下标偏移纠正,这种方法比较绕,下面思路二同样算法的实现比这个简单易理解,程序也更好写。实现代码如下:

/*** 产生随机数组顺序1 ***/
#define LENGTH 25 // 数组长度
#include <time.h>
#include <vector>
#include<iostream>
using namespace std;

// 随机下标数组
int ArrayIndexNew[LENGTH] = {0};
// 随机下标数组原始备份
int ArrayIndex[LENGTH] = {0};
// 恢复完成的原始下标数组
vector<int> DoneArray;
// 恢复完成的生成下标数组
vector<int> DoneArrayNew;
// 下标偏移纠正量
int offset = 0;

/**
 * 产生一组随机下标
 */
void IndexGenerate() {
    // 生成新的随机下标
    for (int i=0; i<LENGTH; i++) {
        srand(unsigned(time(NULL)));
        ArrayIndexNew[i] = ArrayIndex[i] = rand()%(i+1);
    }
    // 下标恢复
    DoneArray.clear();
    for (int i=LENGTH-1; i>=0; i--) {
        offset = 0;
        for (vector<int>::iterator it = DoneArray.begin(); it != DoneArray.end(); it++) {
            if (ArrayIndexNew[i] >= *it) {
                offset++;
            }
        }
        ArrayIndexNew[i] += offset;
        // 继续恢复相同的下标
        do{
            offset = 0;
            for (vector<int>::iterator it = DoneArrayNew.begin(); it != DoneArrayNew.end(); it++) {
                if (ArrayIndexNew[i] == *it) {
                    offset++;
                }
            }
            ArrayIndexNew[i] += offset;
        }while (offset);
        // 当前下标恢复完成
        DoneArrayNew.push_back(ArrayIndexNew[i]);
        DoneArray.push_back(ArrayIndex[i]);
    }
}

/**
 * 打印数组
 */
void Print() {
    cout<<"原始数组下标:";
    for (int i=0; i<LENGTH; i++) {
        cout<<ArrayIndex[i]<<" ";
    }

    cout<<endl<<"生成数组下标:";
    for (int i=0; i<LENGTH; i++) {
        cout<<ArrayIndexNew[i]<<" ";
    }
    cout<<endl;
}

int main() {
    IndexGenerate();
    Print();
    return 0;
}

这里写图片描述

思路 2


和思路1思想一样,不同的是思路1是找数组中剩下的随机位置将元素依次放入生成的随机位置,这里是找剩下的随机元素依次放入数组中:开始生成0-9的随机下标,将该下标在原数组中的元素放入另一个数组的第一个位置,同时要将原数组中选出的元素删除,原数组的长度-1。然后生成0-8的随机下标选出下一个元素放到新数组的第二个位置,以此类推直到所有原数组的元素全部移到新数组算法结束。

实现代码(简单了不是一点点):

/*** 产生随机数组顺序2 ***/
#define LENGTH 25
#include <time.h>
#include <vector>
#include<iostream>
using namespace std;

// 原数组
vector<int> oldArray;
// 打乱后的数组
vector<int> newArray;

/**
 * 将数组随机打乱
 */
void RandomArray(vector<int>oldArray, vector<int> &newArray) {
    // 随机打乱
    for (int i=LENGTH; i>0; i--) {
        srand(unsigned(time(NULL)));
        // 选中的随机下标
        int index = rand()%i;
        // 根据选中的下标将原数组选中的元素push到新数组
        newArray.push_back(oldArray[index]);
        // 将原数组中选中的元素剔除
        oldArray.erase(oldArray.begin()+index);
    }
}

/**
 * 打印数组
 */
void Print() {
    cout<<"原数组:";
    for (vector<int>::iterator it = oldArray.begin() ; it!=oldArray.end() ; it++) {
        cout<<*it<<" ";
    }

    cout<<endl<<"打乱后的数组:";
    for (vector<int>::iterator it = newArray.begin() ; it!=newArray.end() ; it++) {
        cout<<*it<<" ";
    }
    cout<<endl;
}

int main() {
    // 初始化原数组
    for(int i=0; i<LENGTH ;i++)
        oldArray.push_back(i);
    // 打乱
    RandomArray(oldArray, newArray);
    // 打印结果
    Print();
    return 0;
}

这里写图片描述

这里写图片描述

思路三

对思路二的简单空间优化:
第一个循环从所有元素中随机选择一个元素,和最后一个元素交换,如果是最后一个元素本身则不用交换;
第二个循环从前n-1个元素随机选择一个和倒数第二个元素交换,如果是倒数第二个本身则不交换;
以此类推直到只剩第一个元素结束。
这样避免了额外开辟空间。

证明算法可以让每个元素每次都拥有相同的概率占到某个位置(算法是百分百随机的!!!)

这里还是以思路1中10个元素的原数组为例:

1)先看第一个元素在新数组(长度和原数组相等)中某个位置i的概率P1i,明显是1/10(假设计算机生成的随机数足够随机);

2)第二个元素在原数组中的某个位置i的概率P2i是首先第一个元素没有在该位置i的情况发生的情况下,第二个元素在该位置i的概率,第一个元素不在位置i的概率是1-P1i=9/10,此时第二个元素在位置i的概率为1/9(除了位置i剩下的位置有一个被第一个数占了,还剩9个位置),那么这两种情况同时发生的概率最终就是:(9/10) * (1/9) = 1/10;

这里用到的条件概率公式,上面假设:P(A) = 第一个元素不在位置i的概率,P(B|A)=在第一个元素不在位置i的情况下第二个元素在位置i的概率,要求的是P(AB)=第一个元素不在位置i且第二个元素恰好在位置i的概率。

条件概率公式:P(B|A) = P(AB) / P(A)

所以: P(AB) = P(A) * P(B|A)

3)以此类推,第三个元素在位置i的概率为: (9/10) * (8/9) * (/18) = 1/10, 所以数组中每个元素在位置i的概率都是1/10,每个元素的机会是平等的。

4)思路2的证明方法和这个大同小异,元素和位置的角色换一下即可;

用数组打乱算法来对一个魔术做模拟求概率

有一个扑克牌魔术是这样:让观众随机说两个1-13的数(1-13对应于扑克牌13种牌色),然后魔术师洗牌后,那两个数会挨在一起。

这是一个成功率很高但不是百分之百的魔术,既然成功率不是百分百那就是利用了概率了,也就是说,一副扑克牌里面任意两张不一样的牌挨在一起的概率很高,那概率有多高呢?

如果用排列组合概率统计的方法来算难度实在太大,因为不管是两张牌挨在一起还是反面两张牌不挨在一起的情况都太多,一般人恐怕算不出来。有了上面的数组打乱算法现在就可以进行计算机模拟,求得概率的大体值。当然上面的方法得到的随机结果的随机性也有限,对结果精确度的计算自然会有很大影响,这里先不深入考虑。

模拟的方法很简单,定义一个长度为52的数组,表示52张牌(除去大小王),数组元素是1-13的数各4个,然后进行十万或者更多次数的循环,每次随机打乱数组的顺序,统计某两个数挨在一起的次数,然后除以循环次数得到大致的概率值。

发布了109 篇原创文章 · 获赞 403 · 访问量 88万+

猜你喜欢

转载自blog.csdn.net/cordova/article/details/52884399
今日推荐