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;
}
這樣可以萬無一失了……