楠木軒

C/C++編程筆記:C語言 rand() 隨機函數,深入解析程序隨機數

由 諸葛寒香 發佈於 科技

各種編程語言返回的隨機數(確切地説是偽隨機數)實際上都是根據遞推公式計算的一組數值,當序列足夠長,這組數值近似滿足均勻分佈。

C的標準函數庫提供一隨機數生成器rand(定義在stdlib.h),能返回0~RAND_MAX之間均勻分佈的偽隨機整數(RAND_MAX至少為32767,一般都默認為32767)。

用rand()隨機生成在[x,y]內的整數

int k;

k=x+rand()%(y-x+1),k即為所求範圍內隨機生成的數,rand()%a的結果最大為a-1.

rand( )%20的意思的生成20以內的隨機數。

例如:

#include

#include

void main()

{

for(int i=0;i<10;i++)

printf("%d\n",rand());

}

如果我們是第一次運行,而且對其不太清楚,那麼它生成的基本上算是0-RAND_MAX之間的等概率隨機數列了。但是如果你第二次運行的時候會發現輸出結果仍和第一次一樣。

原來rand()生成偽隨機數時需要一個種子(計算偽隨機序列的初始數值)才行,如果種子相同就會得到相同的序列結果(這就是函數的好處T-T)。這個“優點”被有的軟件利用於加密和解密。加密時,可以用某個種子數生成一個偽隨機序列並對數據進行處理;解密時,再利用種子數生成一個偽隨機序列並對加密數據進行還原。這樣,對於不知道種子數的人要想解密就需要多費些事了。當然,這種完全相同的序列對於你來説是非常糟糕的。要解決這個問題,需要在每次產生隨機序列前,先指定不同的種子,這樣計算出來的隨機序列就不會完全相同了。

srand()用來設置rand()產生隨機數時的隨機數種子。在調用rand()函數產生隨機數前,必須先利用srand()設好隨機數種子seed,如果未設隨機數種子,rand()在調用時會自動設隨機數種子為1(有人説默認是0,困惑中)。上面的兩個例子就是因為沒有設置隨機數種子,每次隨機數種子都自動設成相同值1,進而導致rand()所產生的隨機數值都一樣。(可能有人知道C語言中的隨機函數random,可是random函數並不是ANSIC標準,所以説,random函數不能在gcc,vc等編譯器下編譯通過。我們可以自己編一個^0^)我們需要使程序每一次使用的種子都不一樣,現在主要問題是種子srand的選擇是不是接近隨機(不存在完全隨機),你也可以認為指定種子數。

Windows9x/NT的遊戲FreeCell就允許用户指定種子數,這樣用户如果一次遊戲沒有成功,下次還可以以同樣的發牌結果再玩一次。但我們還是喜歡系統自動生成……最簡單的方法就是利用系統時間了(幾乎所有的人都這麼做),因為時間的數值隨時間變化而變化,運行兩次,一般不會出現前一次和後一次相同的局面,time(NULL)會返回一個表示當前系統時間的整數(它在time.h中,據説time()函數求出來的是自1970年1月1日到現在的秒數,有的説是unix元年,不知道這兩個是不是一天……:(,另外有的嫌麻煩會寫作time(0))。我們這麼來寫:

#include

#include

#include

void main()

{

srand((int)time(0));

for(int x=0;x<10;x++)

printf("%d\n",(rand());

}

據説如果軟件一直開兩天,種子會有1/(60*60*24)個可能會重複,雖説這對於一般人已經絕對足夠了,但縱然人不舒服,於是我在網上開到有這麼寫的:

#include

#include

#include

#include

void main(void)

{

int i;

unsigned int seedVal;

struct timeb timeBuf;

ftime(&timeBuf;);

seedVal=((((unsigned int)timeBuf.time&0xFFFF)+

(unsigned int)timeBuf.millitm)^

(unsigned int)timeBuf.millitm);

srand((unsigned int)seedVal);

for(i=0;i<10;++i)

printf("%6d\n",rand());

}

“上面的程序先是調用_ftime()來檢查當前時間,並把它的值存入結構成員timeBuf.time中,當前時間的值從1970年1月1日開始以秒計算。在調用了_ftime()之後,在結構timeBuf的成員millitm中還存入了當前那一秒已經度過的毫秒數,但在DOS中這個數字實際上是以百分之一秒來計算的。然後,把毫秒數和秒數相加,再和毫秒數進行異或運算。當然也可以對這兩個結構成員進行更多的計算,以控制seedVal的取值範圍,並進一步加強它的隨機性,但上例用的邏輯運算就已經足夠了。”

看下面代碼:

void main()

{

for(int i=0;i<100000;i++)

{

srand( (unsigned)time( NULL ) );

printf("%d\n",rand());

}

}

為什麼總是生成一個數???因為此程序產生一個隨機數之前,都調用一次srand,而由於計算機運行很快,所以每用time得到的時間都是一樣的(time的時間精度較低,只有55ms)。這樣相當於使用同一個種子產生隨機序列,所以產生的隨機數總是相同的。把srand放在循環外看看:

srand( (unsigned)time( NULL ) );

for(int i=0;i<100000;i++)

問題解決了:)

既然生成的是

0-RAND_MAX之間均勻分佈的隨機整數(我們姑且把以上方法生成的是理想的隨機數吧),那麼要生成0-x之間(包括0不包括x)的隨機數就把rand()改成

rand()/(double)RAND_MAX*x ,要生成x-y之間(包括x不包括y)的就是

rand()/(double)RAND_MAX*(y-x)+x 了。但是如果要生0-10之間的整數很多人會這麼寫:

#include

#include

#include

void main()

{

srand((int)time(0));

for(int x=0;x<10;x++)

printf("%d\n",(rand()%10);

}

x-y的就是 rand()%(y-x)+x

???懂一點概率的就知道這樣寫,會使得到的數列分佈不均的,但是大家確實都喜歡這麼寫……因為在x的值比較小,RAND_MAX相對較大,而生成的數列又不算太大,又是用來解決精確度要求不高的問題(如一些遊戲目標,傳説遊戲中踩地雷式的遇敵就是用rand()實現的.

當主角在地圖上走的時候動不動冒出三兩小兵挑釁兼找死.它的實現方式是設主角所立位置為0,主角每走一步,變量加1,當變量==隨機取得的數時,小兵出現),這樣寫足夠了……

下面再討論生成m個小於n的不重複隨機數的算法

本人認為算法結構很重要,但語句結構也很重要,所以我喜歡一個算法給出多個語句結構……

最容易想到的傻瓜算法,逐個產生這些隨機數,每產生一個,都跟前面的隨機數比較,如果重複,就重新產生:

算法一(1)

srand((unsigned)time(NULL));

for(j = 0; j < m; j++) {

a:a[j] = rand() % n;

for(i=0;i

很早的時候學編程都喜歡用goto語句,因為過去算法是用流程圖表示的,用goto可以直接轉譯,而且循環語句發展不完善,僅僅是作為條件分支的一個補充,如果條件分支箭頭向上就是一個循環,而且goto可以實現過去循環所不能實現的結構,形成很巧妙的循環交叉。所以過去一般認為有兩種結構,順序和分支,循環是分支的特殊情況。但是break和continue語句使這些結構實現成為可能,後來發現goto的使用會造成維護和調試的許多麻煩,所以循環逐漸代替了goto,使程序更有層次。現在編程不建議用goto了,如果用goto還會被認為編程修養不好……言歸正題,把它的goto去掉:

算法一(2)

srand((unsigned)time(NULL));

j=0;

while (j

{

a[j] = rand() % n ;

for(i=0;i

if(i

j++

}

但是後來都建議用for循環,這樣可以使循環條件更緊湊:

算法一(3)

srand((unsigned)time(NULL));

for(j=0;j<m;j++)

{a[j]=rand()%n;

for(i=0;i<j;i++)

{if(a[i]==a[j])

{i=-1;a[j]=rand()%n;}

}

}

但這是個很笨的方法,且比較次數呈線性增長,越往後次數越多……另一個想法是生成一個就把它從中去掉,就可以實現不重複了:

算法二(1)

for (i=0;i<n;i++)

a[i]=i+1;

for(i=0;i<m;i++)

{j=rand()%(n-i);

b[i]=a[j];

for (k=j;k<n-i-2;k++)

a[k]=a[k+1];

}

b[]是生成的隨機數列

這樣做也太麻煩了,生成的直接做個標記不就行了?

算法三(1-1)

i=rand()%m;

a[i]=1;b[0]=i;

for(j=1;j<m;j++)

{for (i=rand()%m;a[i]==1;i=rand()%m);

b[j]=i;a[i]=1;

}

寫緊湊一些吧,直接:

算法三(1-2)

for(j=0;j<m;j++)

{for (i=rand()%m;a[i]==1;i=rand()%m);

b[j]=i;a[i]=1;

}

但是我看到了更簡潔的:

int n=某值;

int a[n]={0};

for (i=1;i<=n;i++)

{while(a[x=rand()%n]);

a[x]=i;

}

這個無循環體的while保證a[m]是初始化的0狀態。這生成了n個,我們只取m個,這太浪費了,結合一下:

int n=某值,m=某值;

int a[n]={0},b[m]

for (i=1;i<=n;i++)

{while(a[x=rand()%n]);

b[i]=x;a[x]=1;

}

但是總叫人不舒服,對這種算法誰有更好的寫法呢?

這種算法越到後面,遇到已使用過的元素的可能性越高,重複次數就越多,但是當m較小時還是很好的:)

這都不太讓人滿意麼?看看下面這個:

算法四(1)

for (i=0;i<n;i++)

a[i]=i+1;

for (i=n-1;i>0;i--)

{w=rand()%i;

t=a[i];

a[i]=a[w];

a[w]=t;

}

這個算法很不錯,有人會懷疑其隨機性,但個人認為是沒問題的,首先第二行按順序用0到n填滿整個數組;第三行,是隨機產生從0到n-2個數組下標,把這個下標的元素值跟n-1下標的元素值交換,一直進行到下標為1的元素。因此它只需要遍歷一次就能產生全部的隨機數。

但這樣會生成n個小於n的隨機數,我們只要m個,再加兩句:

for(i=0;i<m;i++)

b[i]=a[i];

b[]是生成的隨機數列

如果m和n很接近的話或者相等是最好,但可能不是……那改一下:

算法四(2)

for (i=0;i<n;i++)

a[i]=i+1;

for (i=0;i<m;i++)

{w=rand()%n;

t=a[i];

a[i]=a[w];

a[w]=t;

}

但是條件反過來了,如果m遠小於n還行,如果接近,其隨機性就存在問題了(為什麼?),再改一下:

算法四(3)

for (i=0;i<n;i++)

a[i]=i+1;

for (i=0;i<m;i++)

{w=rand()%(n-i)+i;

t=a[i];

a[i]=a[w];

a[w]=t;

}

這樣可以萬無一失了……