高效生成随机数组算法在实际的程序开发中也大量的进行使用。例如在棋牌类的游戏开发中的斗地主、双扣等,在每次游戏开始时,都需要打乱扑克牌的顺序,然后再将打乱以后的扑克牌依次发放给每个游戏玩家。而这种打乱扑克牌顺序的算法通常就被称作洗牌算法。
从程序开发的角度看洗牌算法,实际上生成的就是一组规则的随机数字,例如对于一副扑克牌来说,在程序中如果需要代表一副扑克牌,根据扑克牌的特点(每幅牌包含54张不同的牌)则只需要为每张牌进行编号即可,例如将扑克牌中的每张牌依次对应编号成1-54之间的整数,那么一副牌就是一个包含1-54之间所有整数的数组。而洗牌就是将这样一个规则的数组变成一个随机数组,但是必须包含所有1-54之间的所有整数。
其中一种实现方案为:
- 随机产生一个1-n的数x,然后让第x张牌和第1张牌互相调换。
- 随机产生一个1-n的数y,然后让第y张牌和第2张牌互相调换。
- 随机产生一个1-n的数z,然后让第z张牌和第i张牌互相调换。(i=3,4,5...54)
这种算法的时间复杂度为O(N)。
这种方案乍看好像不错,网上也有很多文章使用这种算法,但是其实这是一种错误的方法,因为这种方案的所有可能性为N^N,而洗好的牌一种有N!种可能,又因为N^N % N! !=0,所以每种结果的概率是不相同的。
那么如何修正这个问题呢?第i次洗牌不是产生一个1-n的随机数,而是产生一个i-n的随机数,这样可能性结果的可能性就是N!了。就有可能概率相等了。证明在《计算机程序设计艺术》上。
示例代码如下:
#include <time.h>
#include <cstdlib>
#include <iostream>
using namespace std;
#define swap(a,b) {int t=(a);(a)=(b);(b)=(t);}
class Poker
{
private:
static const int poker_size = 54;
int poker[poker_size];
public:
Poker()
{
Init();
}
~Poker(){}
void Init()
{
for(int i = 0; i < poker_size; i++)
{
poker[i] = i + 1;
}
}
void Show()
{
for(int i = 0; i < poker_size; i++)
{
if(i != 0 && i % 10 == 0) printf("\n");
printf("%5d", poker[i]);
}
printf("\n");
}
void Shuffle()
{
srand((int)time(0));
for(int i = 0; i < poker_size; i++)
{
// 随机下标为[0,poker_size - 1.0]之间的随机数,与下标i数据交换
//int ranIndex = 0 +
//(int)((poker_size - 1.0 - 0 + 1.0) * rand() / (RAND_MAX + 1.0));
// 改正后,随机下标为[i,poker_size - 1.0]之间的随机数,与下标i数据交换
int ranIndex = i +
(int)((poker_size - 1.0 - i + 1.0) * rand() / (RAND_MAX + 1.0));
if(i != ranIndex)
{
swap(poker[i], poker[ranIndex]);
}
}
}
};
int main()
{
// 创建并初始化poker
Poker poker;
// 洗牌前,扑克按升序排列:
cout << "Before Shufflling:" << endl;
poker.Show();
// 洗牌
poker.Shuffle();
// 先牌后,扑克顺序被打乱
cout << "After shufflling:" << endl;
poker.Show();
return 0;
}
一种可能的结果为:
参考自: