真正意義上的隨機數(或者隨機事件)在某次產生過程中是按照實驗過程中表現的分布機率隨機產生的,其結果是不可預測的,是不可見的。而電腦中的隨機函數是按照一定演算法類比產生的,其結果是確定的,是可見的。我們可以這樣認為這個可預見的結果其出現的機率是100%。所以用電腦隨機函數所產生的“隨機數”並不隨機,是偽隨機數。
產生方法
一般地,偽隨機數的產生方法主要有以下3種:
(1) 直接法(Direct Method),根據分布函數的物理意義產生。缺點是僅適用於某些具有特殊分布的隨機數,如二項式分布、泊松分布。
(2) 逆轉法(Inversion Method),假設U服從[0,1]區間上的均勻分布,令X=F-1(U),則X的累計分布函數(CDF)為F。該方法原理簡單、編程方便、適用性廣。
(3)接受拒絕法(Acceptance-Rejection Method):假設希望產生的隨機數的機率密度函數(PDF)為f,則首先找到一個PDF為g的隨機數發生器與常數c,使得f (x)≤cg(x),然後根據接收拒絕演算法求解。由於演算法平均運算c次才能得到一個希望產生的隨機數,因此c的取值必須儘可能小。顯然,該演算法的缺點是較難確定g與c。
因此,偽隨機數產生器(PRNG)一般採用逆轉法,其基礎是均勻分布,均勻分布PRNG的優劣決定了整個隨機數體系的優劣[7]。下文研究均勻分布的PRNG。
程式執行個體
C語言程式例
下面看這樣一個C程式:
//rand01.c
#include
static unsigned int RAND_SEED;
unsigned int random(void)
{
RAND_SEED=(RAND_SEED*123+59)%65536;
return(RAND_SEED);
}
void random_start(void)
{
int temp[2];
movedata(0x0040,0x006c,FP_SEG(temp),FP_OFF(temp),4);
RAND_SEED=temp[0];
}
main()
{
unsigned int i,n;
random_start();
for(i=0;i<10;i++)
printf("%u\t",random());
printf("\n");
}
這個程式(rand01.c)完整地闡述了隨機數產生的過程:
首先,主程式調用random_start()方法,random_start()方法中的這一句我高度興趣:
movedata(0x0040,0x006c,FP_SEG(temp),FP_OFF(temp),4);
這個函數用來移動記憶體資料,其中FP_SEG(far pointer to segment)是取temp數組段地址的函數,FP_OFF(far pointer to offset)是取temp數組相對地 址的函數,movedata函數的作用是把位於0040:006CH儲存單元中的雙字放到數組temp的聲明的兩個儲存單元中。這樣可以通過temp數組把0040:006CH處的一個16位的數送給RAND_SEED。
random用來根據隨機種子RAND_SEED的值計算得出隨機數,其中這一句:
RAND_SEED=(RAND_SEED*123+59)%65536;
是用來計算隨機數的方法,隨機數的計算方法在不同的電腦中是不同的,即使在相同的電腦中安裝的不同的作業系統中也是不同的。我在linux和windows下分別試過,相同的隨機種子在這兩種作業系統中產生的隨機數是不同的,這說明它們的計算方法不同。
現在,我們明白隨機種子是從哪兒獲得的,而且知道隨機數是怎樣通過隨機種子計算出來的了。那麼,隨機種子為什麼要在記憶體的0040:006CH處取?0040:006CH處存放的是什嗎?
學過《電腦群組成原理與介面技術》這門課的人可能會記得在編製ROM BIOS時鐘中斷服務程式時會用到Intel 8253定時/計數器,它與Intel8259中斷晶片的通訊使得中斷服務程式得以運轉,主板每秒產生的18.2次中斷正是處理器根據定時/記數器值控制中斷晶片產生的。在我們電腦的主機板上都會有這樣一個定時/記數器用來計算當前系統時間,每過一個時鐘訊號周期都會使記數器加一,而這個記數器的值存放在哪兒呢?沒錯,就在記憶體的0040:006CH處,其實這一段記憶體空間是這樣定義的:
TIMER_LOW DW ? ;地址為 0040:006CH
TIMER_HIGH DW ? ;地址為 0040:006EH
TIMER_OFT DB ? ;地址為 0040:0070H
時鐘中斷服務程式中,每當TIMER_LOW轉滿時,此時,記數器也會轉滿,記數器的值歸零,即TIMER_LOW處的16位二進位歸零,而TIMER_HIGH加一。rand01.c中的
movedata(0x0040,0x006c,FP_SEG(temp),FP_OFF(temp),4);
正是把TIMER_LOW和TIMER_HIGH兩個16位位元放進temp數組,再送往RAND_SEED,從而獲得了“隨機種子”。
現在,可以確定的一點是,隨機種子來自系統時鐘,確切地說,是來自電腦主板上的定時/計數器在記憶體中的記數值。這樣,我們總結一下前面的分析,並討論一下這些結論在程式中的應用:
1.隨機數是由隨機種子根據一定的計算方法計算出來的數值。所以,只要計算方法一定,隨機種子一定,那麼產生的隨機數就不會變。
C++程式例
看下面這個C++程式:
//rand02.cpp
偽隨機數理論推導
#include
#include
using namespace std;
int main()
{
unsigned int seed=5;
srand(seed);
unsigned int r=rand();
cout<<"r = "<<r<<endl; //[已更新]已補全 編輯者註:可能代碼有缺 return 0;
}
在相同的平台環境下,編譯產生exe後,每次運行它,顯示的隨機數都是一樣的。這是因為在相同的編譯平台環境下,由隨機種子產生隨機數的計算方法都是一樣的,再加上隨機種子一樣,所以產生的隨機數就是一樣的。
2.只要使用者或第三方不設定隨機種子,那麼在預設情況下隨機種子來自系統時鐘(即定時/計數器的值)
C++程式例2
看下面這個C++程式:
//rand03.cpp
#include <iostream>
#include <cstdlib>
using namespace std;
int main()
{
srand((unsigned)time(NULL));
unsigned int r=rand();
cout<<"r = "<<r<<endl; //[已更新]已補全 編輯者註:可能代碼有缺
return 0;
}
這裡使用者和其他程式沒有設定隨機種子,則使用系統定時/計數器的值做為隨機種子,所以,在相同的平台環境下,編譯產生exe後,每次運行它,顯示的隨機數會是偽隨機數,即每次運行顯示的結果會有不同。
3.建議:如果想在一個程式中產生隨機數序列,需要至多在產生隨機數之前設定一次隨機種子。
產生一個隨機字串
看下面這個用來產生一個隨機字串的C++程式:(原來的程式我編譯不了,就改了改,加了一些標頭檔)
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
#include<stdlib.h>
#include<time.h>
#include<cmath>
using namespace std;
inline void keep_window_open(){char ch;cin>>ch;}
#define RAND_MAX 0x7fff
int main()
{
int rNum=0;
int m=20;
char *ch=new char[m];
srand((unsigned)time(NULL));
for(int i=0;i<m;++i){
rNum=1+(int)((rand()/(double)RAND_MAX)*36);
switch(rNum){
case 1:ch[i]='a';
break;
case 2:ch[i]='b';
break;
case 3:ch[i]='c';
break;
case 4:ch[i]='d';
break;
case 5:ch[i]='e';
break;
case 6:ch[i]='f';
break;
case 7:ch[i]='g';
break;
case 8:ch[i]='h';
break;
case 9:ch[i]='i';
break;
case 10:ch[i]='j';
break;
case 11:ch[i]='k';
break;
case 12:ch[i]='l';
break;
case 13:ch[i]='m';
break;
case 14:ch[i]='n';
break;
case 15:ch[i]='o';
break;
case 16:ch[i]='p';
break;
case 17:ch[i]='q';
break;
case 18:ch[i]='r';
break;
case 19:ch[i]='s';
break;
case 20:ch[i]='t';
break;
case 21:ch[i]='u';
break;
case 22:ch[i]='v';
break;
case 23:ch[i]='w';
break;
case 24:ch[i]='x';
break;
case 25:ch[i]='y';
break;
case 26:ch[i]='z';
break;
default:cout<<"no!\n";
break;
}
cout<<ch[i]<<'\n';
}
return 0;
}
而運行結果顯示的隨機字串的每一個字元都是一樣的,也就是說產生的字元序列不隨機,所以我們需要把srand((unsigned)time(NULL)); 從for迴圈中移出放在for語句前面,這樣可以產生隨機的字元序列,而且每次運行產生的字元序列會不同(呵呵,也有可能相同,不過出現這種情況的幾率太小了)。
如果你把srand((unsigned)time(NULL));改成srand(2);這樣雖然在一次運行中產生的字元序列是隨機的,但是每次運行時產生的隨機字元序列串是相同的。把srand這一句從程式中去掉也是這樣。
此外,你可能會遇到這種情況,在使用timer控制項編製程式的時候會發現用相同的時間間隔產生的一組隨機數會顯得有規律,而由使用者按鍵command事件產生的一組隨機數卻顯得比較隨機,為什嗎?根據我們上面的分析,你可以很快想出答案。這是因為timer是由電腦時鐘記數器精確控制時間間隔的控制項,時間間隔相同,記數器前後的值之差相同,這樣時鐘取值就是呈線性規律的,所以隨機種子是呈線性規律的,產生的隨機數也是有規律的。而使用者按鍵事件產生隨機數確實更呈現隨機性,因為事件是由人按鍵引起的,而人不能保證嚴格的按鍵時間間隔,即使嚴格地去做,也不可能完全精確做到,只要時間間隔相差一微秒,記數器前後的值之差就不相同了,隨機種子的變化就失去了線性規律,那麼產生的隨機數就更沒有規律了,所以這樣產生的一組隨機數更隨機。這讓我想到了各種晚會的抽獎程式,如果用人來按鍵產生幸運觀眾的話,那就會很好的實現隨機性原則,結果就會更公正。
總結
1.電腦的偽隨機數是由隨機種子根據一定的計算方法計算出來的數值。所以,只要計算方法一定,隨機種子一定,那麼產生的隨機數就是固定的。
2.只要使用者或第三方不設定隨機種子,那麼在預設情況下隨機種子來自系統時鐘。
偽隨機數產生器的缺點
重複做N=10000次實驗,每次產生S=20與S=100個隨機分布的樣本,同時採用Kolmogorov- Smirnov假設檢驗(hypothesis test)來確定樣本是否滿足均勻分布。規定:
① 0假設(nullhypothesis)為樣本服從均勻分布;② 1假設(alternativehypothesis)為樣本不服從均勻分布。
採用P值(∈[0, 1])衡量,P值越趨近於0,表示越有理由拒絕0假設,即樣本不服從均勻分布;P值越趨近於1,表示越有理由接受0假設,即樣本服從均勻分布。
1與圖2所示:隨著P值下降,樣本也越來越不服從均勻分布。實踐中希望P值越大越好。然而統計學的結論顯示,P值一定服從均勻分布,與N、S大小無關,這表明由於隨機性,總會出現某次抽樣得到的樣本不服從、甚至遠離均勻分布。另外,樣本大小的不同,造成檢驗標準的不同,直觀上看S=100對應的均勻分布普遍比S=20對應的更均勻。因此,小樣本情況下均勻分布PRNG的差異性尤為嚴重。