高效生成随机数组算法在实际的程序开发中也大量的进行使用。例如在棋牌类的游戏开发中的斗地主、双扣等,在每次游戏开始时,都需要打乱扑克牌的顺序,然后再将打乱以后的扑克牌依次发放给每个游戏玩家。而这种打乱扑克牌顺序的算法通常就被称作洗牌算法。
从程序开发的角度看洗牌算法,实际上生成的就是一组规则的随机数字,例如对于一副扑克牌来说,在程序中如果需要代表一副扑克牌,根据扑克牌的特点(每幅牌包含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; }
一种可能的结果为:
参考自: