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