转自:http://blog.sina.com.cn/s/blog_6ae5ed170100q3jg.html
本文要解决的问题是:
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次。
当然,这种方法在现实中很难做到,很费时间,但人机有别,在计算机看来,这是最受欢迎的方法!下面给出这种方法的算法。
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中有这样的珍宝。