对C++的整数数组进行洗牌

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在本程序中已经接近上限。

你可能感兴趣的:(C++,算法,Integer,Random,Numbers)