計算機隨機數的產生 (轉)

amyz發表於2007-08-17
計算機隨機數的產生 (轉)[@more@]

中隨機數的產生

大家可能很多次討論過隨機數在計算機中怎樣產生的問題,在這篇文章中,我會對這個問題進行更深入的探討,闡述我對這個問題的理解。

首先需要宣告的是,計算機不會產生絕對隨機的隨機數,計算機只能產生“偽隨機數”。其實絕對隨機的隨機數只是一種理想的隨機數,即使計算機怎樣發展,它也不會產生一串絕對隨機的隨機數。計算機只能生成相對的隨機數,即偽隨機數。

偽隨機數並不是假隨機數,這裡的“偽”是有規律的意思,就是計算機產生的偽隨機數既是隨機的又是有規律的。怎樣理解呢?產生的偽隨機數有時遵守一定的規律,有時不遵守任何規律;偽隨機數有一部分遵守一定的規律;另一部分不遵守任何規律。比如“世上沒有兩片形狀完全相同的樹葉”,這正是點到了事物的特性,即隨機性,但是每種樹的葉子都有近似的形狀,這正是事物的共性,即規律性。從這個角度講,你大概就會接受這樣的事實了:計算機只能產生偽隨機數而不能產生絕對隨機的隨機數。

那麼計算機中隨機數是怎樣產生的呢?有人可能會說,隨機數是由“隨機種子”產生的。沒錯,隨機種子是用來產生隨機數的一個數,在計算機中,這樣的一個“隨機種子”是一個無符號整形數。那麼隨機種子是從哪裡獲得的呢?

下面看這樣一個C:

//rand01.c
#include

static unsigned int RAND_SEED;

unsigned int ran(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 ",random());
 printf(" ");
}

這個程式(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;

是用來計算隨機數的方法,隨機數的計算方法在不同的計算機中是不同的,即使在相同的計算機中的不同的操作中也是不同的。我在和下分別試過,相同的隨機種子在這兩種中生成的隨機數是不同的,這說明它們的計算方法不同。

現在,我們明白隨機種子是從哪兒獲得的,而且知道隨機數是怎樣透過隨機種子計算出來的了。那麼,隨機種子為什麼要在記憶體的0040:006CH處取?0040:006CH處存放的是什麼?

學過《計算機組成原理與介面技術》這門課的人可能會記得在編制ROM 時鐘中斷服務程式時會用到 8253定時/計數器,它與Intel 8259中斷的通訊使得中斷服務程式得以運轉,主機板每秒產生的18.2次中斷正是根據定時/記數器值控制中斷晶片產生的。在我們計算機的主機板上都會有這樣一個定時/記數器用來計算當前系統時間,每過一個時鐘訊號週期都會使記數器加一,而這個記數器的值存放在哪兒呢?沒錯,就在記憶體的0040:006CH處,其實這一段記憶體空間是這樣定義的:

TIMER_LOW  DW  ?  ;地址為 0040:006CH
TIMER_HIGH  DW  ?  ;地址為 0040:006EH
TIMER_OFT   ?  ;地址為 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++程式:

//rand02.cpp
#include
#include
using namespace std;

int main()
{
 unsigned int seed=5;
 srand(seed);
 unsigned int r=rand();
 cout<}

在相同的平臺環境下,編譯生成exe後,每次執行它,顯示的隨機數都是一樣的。這是因為在相同的編譯平臺環境下,由隨機種子生成隨機數的計算方法都是一樣的,再加上隨機種子一樣,所以產生的隨機數就是一樣的。

2.只要或第三方不設定隨機種子,那麼在預設情況下隨機種子來自系統時鐘(即定時/計數器的值)

看下面這個C++程式:

//rand03.cpp
#include
#include
using namespace std;

int main()
{
 srand((unsigned)time(NULL));
 unsigned int r=rand();
 cout< return 0;
}

這裡使用者和其他程式沒有設定隨機種子,則使用系統定時/計數器的值做為隨機種子,所以,在相同的平臺環境下,編譯生成exe後,每次執行它,顯示的隨機數會是偽隨機數,即每次執行顯示的結果會有不同。

3.建議:如果想在一個程式中生成隨機數序列,需要至多在生成隨機數之前設定一次隨機種子。

看下面這個用來生成一個隨機字串的C++程式:

//rand04.cpp
#include
#include
using namespace std;
int main()
{
 int rNum,m=20;
 char *ch=new char[m];
 
 for ( int i = 0; i //大家看到了,隨機種子會隨著for迴圈在程式中設定多次
 srand((unsigned)time(NULL));
 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 ;
 case 27:ch[i]='0';
 break;
 case 28:ch[i]='1';
 break;
 case 29:ch[i]='2';
 break;
 case 30:ch[i]='3';
 break;
 case 31:ch[i]='4';
 break;
 case 32:ch[i]='5';
 break;
 case 33:ch[i]='6';
 break;
 case 34:ch[i]='7';
 break;
 case 35:ch[i]='8';
 break;
 case 36:ch[i]='9';
 break;
 }//end of switch
 cout< }//end of for l
 
 cout< return 0;
}

而執行結果顯示的隨機字串的每一個字元都是一樣的,也就是說生成的字元序列不隨機,所以我們需要把srand((unsigned)time(NULL)); 從for迴圈中移出放在for語句前面,這樣可以生成隨機的字元序列,而且每次執行生成的字元序列會不同(呵呵,也有可能相同,不過出現這種情況的機率太小了)。
如果你把srand((unsigned)time(NULL));改成srand(2);這樣雖然在一次執行中產生的字元序列是隨機的,但是每次執行時產生的隨機字元序列串是相同的。把srand這一句從程式中去掉也是這樣。

此外,你可能會遇到這種情況,在使用timer編制程式的時候會發現用相同的時間間隔生成的一組隨機數會顯得有規律,而由使用者按鍵command事件產生的一組隨機數卻顯得比較隨機,為什麼?根據我們上面的分析,你可以很快想出答案。這是因為timer是由計算機時鐘記數器精確控制時間間隔的控制元件,時間間隔相同,記數器前後的值之差相同,這樣時鐘取值就是呈線性規律的,所以隨機種子是呈線性規律的,生成的隨機數也是有規律的。而使用者按鍵事件產生隨機數確實更呈現隨機性,因為事件是由人按鍵引起的,而人不能保證嚴格的按鍵時間間隔,即使嚴格地去做,也不可能完全精確做到,只要時間間隔相差一微秒,記數器前後的值之差就不相同了,隨機種子的變化就失去了線性規律,那麼生成的隨機數就更沒有規律了,所以這樣生成的一組隨機數更隨機。這讓我想到了各種晚會的抽獎程式,如果用人來按鍵產生幸運觀眾的話,那就會很好的實現隨機性原則,結果就會更公正。

最後,我總結兩個要點:
1.計算機的偽隨機數是由隨機種子根據一定的計算方法計算出來的數值。所以,只要計算方法一定,隨機種子一定,那麼產生的隨機數就是固定的。
2.只要使用者或第三方不設定隨機種子,那麼在預設情況下隨機種子來自系統時鐘。

(完)


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-963177/,如需轉載,請註明出處,否則將追究法律責任。

相關文章