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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章