C++的algorithm标准库中有一个random_shuffle()函数,可以随机打乱vector元素的顺序(在扑克游戏中称为洗牌)。但对于数组,却没有这个便利的工具可用。
本文要解决的问题是:
1. 给定一个整数数组,如何打乱该数组的顺序?
2. 如何确定算法的效率?
1. 算法的实现
《Beginning Microsoft Visual C# 2008》一书中有一种算法,我把它改写为C++的形式如下:
const int ARRAY_SIZE = 54;
void CheckedShuffle(int* theArray)
{
int newArray[ARRAY_SIZE];
bool assigned[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; i++)
{
assigned[i] = false;
}
for (int i = 0; i < ARRAY_SIZE; i++)
{
int destIndex = 0;
bool foundIndex = false;
while (foundIndex == false)
{
destIndex = rand() % ARRAY_SIZE;
if (assigned[destIndex] == false)
foundIndex = true;
}
assigned[destIndex] = true;
newArray[destIndex] = theArray[i];
}
memcpy(theArray, newArray, sizeof(newArray));
}
这种算法的思路是,对于数组中每个元素,先产生一个随机数,以这个随机数作为目标数组的索引值,将该元素复制至目标数组中。例如,第一个数组元素值为0,所产生的随机数为27,则将目标数组的第27个元素值设为0. 这种算法还使用了一个assigned数组记录已经产生过的随机数。对于每个新产生的随机数,则将assigned数组相应的位置设为true,这样,对于以后产生随机数,只要在assigned数组的对应位置的值为false,就可以复制数组元素了。
这种算法实现起来并不困难,但算法并不高效,尤其是数组越大,越到最后,产生的废弃随机数就越多。例如,对于元素个数为54的数组,假设所有的随机数已经产生,就差39了。代码先产生一个随机数,如20,由于新数组中该位置已经有元素,则废弃20,再产生一个随机数,再比较,再废弃,直到最终产生了39为止。这一步的正确概率为1/54. 数组越大,正确概率就越低,花费的时间就越长。
因为这种算法总是要检查以前产生的随机数,因此我将实现这种算法的函数称为CheckedShuffle.
那么如何避免产生重复的随机数?
玩扑克牌时,一种洗牌的方法是将牌平均分为两摊,左右手各一摊,然后两摊相对,左右手轮流插牌,这样,左边的牌就能与右边的牌相互聚到一起,达到洗牌效果。这种方式使用了交换牌位的方法,简单好用。但由于相邻的牌实际上还是不太分散,因此,效果不是很好。
我这里采用的是一种比较怪异的洗牌方法。54张牌持在手上,由一个忠实的观众先喊出一个1-54的数,如35,则将第35张牌抽出,放在桌面上。再由观众喊出另一个数字。他这时还能喊54吗?不能了,因为手上的牌只剩53张牌,因此,他只能从喊出1-53的数字。这样,桌面上的牌越来越多,而观众能喊的范围的也越来越小。每喊一次,只要该数小于或等于手中持牌数,总是有效的。这样,当手中最后只剩一张牌时,观众就可以领奖退场了。他喊了多少次?最后一张不算,他只喊了53次。[注:这种算法称为Fisher-Yates shuffle]
当然,这种方法在现实中很难做到,很费时间,但人机有别,在计算机看来,这是最受欢迎的方法!下面给出这种方法的算法。
int GetRandNumInRange(int min, int max)
{
int result = rand() % (max - min + 1) + min;
return result;
}
void IndexShuffle(int* theArray)
{
for (int i = 0; i < ARRAY_SIZE - 1; i++)
{
int randomIndex = GetRandNumInRange(i + 1, ARRAY_SIZE - 1);
swap(theArray[i], theArray[randomIndex]);
}
}
这种算法的思路是,对于每一张牌,与该牌其后的任意一张牌交换。例如,第1张牌与第38(随机)张牌交换后,第1张牌就固定下来了,等同于将该牌放至桌面上。然后,第2张与第43张牌交换后放至桌面,如此等等。这样,随机数的范围就从[2, 53]开始,意为从第2张牌开始,在剩下的53张中取一随机数。之后,范围缩小为[3, 53] ...,最后为[53,53].
C++中产生随机函数只有一个rand(),所产生的数值范围为[0, 32767]。当然,很多时候,我们只需要在一个较小的特定范围内产生随机数,此时,可以通过取模的方式实现。
rand() % 100 -> [0, 99]
rand() % 100 + 1 -> [1, 100]
rand() % 30 + 10 -> [0, 29] + 10 -> [0 + 10, 29 + 10] -> [10, 39]
现在,假设我们要求得[10, 39]的随机数,如何反推出rand() % 30 + 10的公式来?
设min = 10, max = 39,
则[10, 39] -> [min, max] -> [0 + min, (max - min) + min] -> [0, (max - min)] + min -> rand() % (max - min + 1) + min.
因此,rand() % (max - min + 1) + min 总能生成[min, max]范围内的随机数。因为此公式表面看来难以理解且令人头晕,因此,我将其重构为一个名为GetRandNumInRange(int, int)的函数。
swap函数在标准库algorithm中,因此不需我们再定义该函数了。
2. 算法的效率
算法出来了,现在我们要比较CheckedShuffle及IndexShufle这两种算法的效率。
我先试用time_t来比较,但很可惜,time_t的精确度只支持到秒数。而这两种算法所花费的时间都是0秒。在计算机世界中,0秒并不等于不费时间,我们需要一个更高精度的时间。
C++的标准库无法支持毫秒级的时间精度。实际上,我们这里的算法需要用微秒来衡量。所幸,Windows API中有这样的珍宝。
QueryPerformanceFrequency函数可取得系统中高精度的时钟频率,以每秒多少次来计算。此频率在系统运行时不会改变。
LARGE_INTEGER liFreq;
if (!QueryPerformanceFrequency(&liFreq))
{
cout << "Your sytem does not support high-resolution performance counter" << endl;
return -1;
}
并非每个系统都能支持这种计时器,因此,QueryPerformanceFrequency函数返回一个bool值。如果成功,则将计数器存放至一个LARGE_INTEGER的变量中。之后,可以使用
LARGE_INTEGER liStart, liEnd;
QueryPerformanceCounter(&liStart);
//job processing......
QueryPerformanceCounter(&liEnd);
分别在工作开始及结束后取得两个计数值。
double dbTimespan;
dbTimespan = (double)(liEnd.QuadPart - liStart.QuadPart);
dbTimespan = dbTimespan / (double)liFreq.QuadPart * 1000000;
使用liEnd.QuadPart - liStart.QuadPart可以取得两个计数的差值,再除以时钟频率,得到的是以秒数计算的时间,这是一个用科学计数法才能方便地表示的值。因为1秒 = 1000毫秒 = 1000000微秒,因此,将其乘以1000000,就可得到比较直观的微秒。
下面是完整代码:
#include <iostream>
#include <time.h>
#include <windows.h>
using namespace std;
const int ARRAY_SIZE = 54;
long lRandNumCreated;
LARGE_INTEGER liFreq;
LARGE_INTEGER liStart, liEnd;
double dbTimespan;
void StartRecordTimeCounter()
{
QueryPerformanceCounter(&liStart);
}
void EndRecordTimeCounter()
{
QueryPerformanceCounter(&liEnd);
}
void ShowTimeElapsed()
{
dbTimespan = (double)(liEnd.QuadPart - liStart.QuadPart);
dbTimespan = dbTimespan / (double)liFreq.QuadPart * 1000000;
cout << endl << lRandNumCreated << " random numbers created" << ", ";
cout << dbTimespan << " microseconds" << " elapsed." << endl;
}
int GetRandNumInRange(int min, int max)
{
int result = rand() % (max - min + 1) + min;
return result;
}
void IndexShuffle(int* theArray)
{
lRandNumCreated = 0;
StartRecordTimeCounter();
for (int i = 0; i < ARRAY_SIZE - 1; i++)
{
int randomIndex = GetRandNumInRange(i + 1, ARRAY_SIZE - 1);
lRandNumCreated++;
swap(theArray[i], theArray[randomIndex]);
}
EndRecordTimeCounter();
}
void CheckedShuffle(int* theArray)
{
int newArray[ARRAY_SIZE];
bool assigned[ARRAY_SIZE];
lRandNumCreated = 0;
StartRecordTimeCounter();
for (int i = 0; i < ARRAY_SIZE; i++)
{
assigned[i] = false;
}
for (int i = 0; i < ARRAY_SIZE; i++)
{
int destIndex = 0;
bool foundIndex = false;
while (foundIndex == false)
{
destIndex = rand() % ARRAY_SIZE;
lRandNumCreated++;
if (assigned[destIndex] == false)
foundIndex = true;
}
assigned[destIndex] = true;
newArray[destIndex] = theArray[i];
}
memcpy(theArray, newArray, sizeof(newArray));
EndRecordTimeCounter();
}
void Display(const int* theArray)
{
for (int i = 0; i < ARRAY_SIZE; i++) {
cout << theArray[i] << " ";
}
cout << endl;
}
int main() {
int a[ARRAY_SIZE];
for (int i = 0; i<ARRAY_SIZE; i++) {
a[i] = i + 1;
}
cout << "Before shuffling:" << endl;
Display(a);
srand((unsigned)time(NULL));
if (!QueryPerformanceFrequency(&liFreq))
{
cout << "Your sytem does not support high-resolution performance counter" << endl;
return -1;
}
IndexShuffle(a);
cout << endl << "After shuffling using IndexShuffle():" << endl;
Display(a);
ShowTimeElapsed();
CheckedShuffle(a);
cout << endl << "After shuffling using CheckedShuffle():" << endl;
Display(a);
ShowTimeElapsed();
return 0;
}
在笔者的电脑上,有如下结果:
IndexShuffle():
53 random numbers created, 6.81985 microseconds elapsed.
CheckedShuffle():
168 random numbers created, 9.37729 microseconds elapsed.
每次运行,除了IndexShuffle()所产生的随机数恒为53之外,其他3个数字的结果都不一样。
若将数组元素值加大,则可以看出两种算法的差距更加明显。但请先将Display函数屏蔽掉,否则,屏幕将因为不停地滚动而只能看至第二次的结果。并且,要将这些数值都打印出来,将花费很长时间。
将ARRAY_SIZE设为5400时,
IndexShuffle():
5399 random numbers created, 584.802 microseconds elapsed.
CheckedShuffle():
47304 random numbers created, 1964.54 microseconds elapsed.
而将ARRAY_SIZE设为30000时,
IndexShuffle():
29999 random numbers created, 3367.3 microseconds elapsed.
CheckedShuffle():
330615 random numbers created, 14329.8 microseconds elapsed.
虽然3367.3微秒与14329.8微秒对人类来说相差不大,但CheckedShuffle函数却产生了33万个随机数!另外需要注意的是,由于C++的rand()所生成的随机数上限为32767,ARRAY_SIZE设为30000在本程序中已经接近上限。