C/C++編程筆記:C語言 rand() 隨機函數(shù),深入解析程序隨機數(shù)!
各種編程語言返回的隨機數(shù)(確切地說是偽隨機數(shù))實際上都是根據(jù)遞推公式計算的一組數(shù)值,當序列足夠長,這組數(shù)值近似滿足均勻分布。
C的標準函數(shù)庫提供一隨機數(shù)生成器rand(定義在stdlib.h),能返回0~RAND_MAX之間均勻分布的偽隨機整數(shù)(RAND_MAX至少為32767,一般都默認為32767)。

用rand()隨機生成在[x,y]內的整數(shù)
int k;
k=x+rand()%(y-x+1),k即為所求范圍內隨機生成的數(shù),rand()%a的結果最大為a-1.
rand(?)%20的意思的生成20以內的隨機數(shù)。
例如:
#include
#include
void main()
{
for(int i=0;i<10;i++)
printf("%d\n",rand());
}
如果我們是第一次運行,而且對其不太清楚,那么它生成的基本上算是0-RAND_MAX之間的等概率隨機數(shù)列了。但是如果你第二次運行的時候會發(fā)現(xiàn)輸出結果仍和第一次一樣。
原來rand()生成偽隨機數(shù)時需要一個種子(計算偽隨機序列的初始數(shù)值)才行,如果種子相同就會得到相同的序列結果(這就是函數(shù)的好處T-T)。這個“優(yōu)點”被有的軟件利用于加密和解密。加密時,可以用某個種子數(shù)生成一個偽隨機序列并對數(shù)據(jù)進行處理;解密時,再利用種子數(shù)生成一個偽隨機序列并對加密數(shù)據(jù)進行還原。這樣,對于不知道種子數(shù)的人要想解密就需要多費些事了。當然,這種完全相同的序列對于你來說是非常糟糕的。要解決這個問題,需要在每次產(chǎn)生隨機序列前,先指定不同的種子,這樣計算出來的隨機序列就不會完全相同了。

srand()用來設置rand()產(chǎn)生隨機數(shù)時的隨機數(shù)種子。在調用rand()函數(shù)產(chǎn)生隨機數(shù)前,必須先利用srand()設好隨機數(shù)種子seed,如果未設隨機數(shù)種子,rand()在調用時會自動設隨機數(shù)種子為1(有人說默認是0,困惑中)。上面的兩個例子就是因為沒有設置隨機數(shù)種子,每次隨機數(shù)種子都自動設成相同值1,進而導致rand()所產(chǎn)生的隨機數(shù)值都一樣。(可能有人知道C語言中的隨機函數(shù)random,可是random函數(shù)并不是ANSIC標準,所以說,random函數(shù)不能在gcc,vc等編譯器下編譯通過。我們可以自己編一個^0^)我們需要使程序每一次使用的種子都不一樣,現(xiàn)在主要問題是種子srand的選擇是不是接近隨機(不存在完全隨機),你也可以人為指定種子數(shù)。
Windows9x/NT的游戲FreeCell就允許用戶指定種子數(shù),這樣用戶如果一次游戲沒有成功,下次還可以以同樣的發(fā)牌結果再玩一次。但我們還是喜歡系統(tǒng)自動生成……最簡單的方法就是利用系統(tǒng)時間了(幾乎所有的人都這么做),因為時間的數(shù)值隨時間變化而變化,運行兩次,一般不會出現(xiàn)前一次和后一次相同的局面,time(NULL)會返回一個表示當前系統(tǒng)時間的整數(shù)(它在time.h中,據(jù)說time()函數(shù)求出來的是自1970年1月1日到現(xiàn)在的秒數(shù),有的說是unix元年,不知道這兩個是不是一天……:(,另外有的嫌麻煩會寫作time(0))。我們這么來寫:
#include
#include
#include
void main()
{
srand((int)time(0));
for(int x=0;x<10;x++)
printf("%d\n",(rand());
}
?據(jù)說如果軟件一直開兩天,種子會有1/(60*60*24)個可能會重復,雖說這對于一般人已經(jīng)絕對足夠了,但縱然人不舒服,于是我在網(wǎng)上開到有這么寫的:
#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中還存入了當前那一秒已經(jīng)度過的毫秒數(shù),但在DOS中這個數(shù)字實際上是以百分之一秒來計算的。然后,把毫秒數(shù)和秒數(shù)相加,再和毫秒數(shù)進行異或運算。當然也可以對這兩個結構成員進行更多的計算,以控制seedVal的取值范圍,并進一步加強它的隨機性,但上例用的邏輯運算就已經(jīng)足夠了?!?/p>
看下面代碼:
void main()
{
for(int i=0;i<100000;i++)
{
srand( (unsigned)time( NULL ) );
printf("%d\n",rand());
}
}
為什么總是生成一個數(shù)???因為此程序產(chǎn)生一個隨機數(shù)之前,都調用一次srand,而由于計算機運行很快,所以每用time得到的時間都是一樣的(time的時間精度較低,只有55ms)。這樣相當于使用同一個種子產(chǎn)生隨機序列,所以產(chǎn)生的隨機數(shù)總是相同的。把srand放在循環(huán)外看看:
srand( (unsigned)time( NULL ) );
for(int i=0;i<100000;i++)
問題解決了:)
既然生成的是
0-RAND_MAX之間均勻分布的隨機整數(shù)(我們姑且把以上方法生成的是理想的隨機數(shù)吧),那么要生成0-x之間(包括0不包括x)的隨機數(shù)就把rand()改成?
rand()/(double)RAND_MAX*x ,要生成x-y之間(包括x不包括y)的就是?
rand()/(double)RAND_MAX*(y-x)+x?? 了。但是如果要生0-10之間的整數(shù)很多人會這么寫:
#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
???懂一點概率的就知道這樣寫,會使得到的數(shù)列分布不均的,但是大家確實都喜歡這么寫……因為在x的值比較小,RAND_MAX相對較大,而生成的數(shù)列有不算太大,又是用來解決精確度要求不高的問題(如一些游戲目標,傳說游戲中踩地雷式的遇敵就是用rand()實現(xiàn)的.
當主角在地圖上走的時候動不動冒出三兩小兵挑釁兼找死.它的實現(xiàn)方式是設主角所立位置為0,主角每走一步,變量加1,當變量==隨機取得的數(shù)時,小兵出現(xiàn)),這樣寫足夠了……

下面再討論生成m個小于n的不重復隨機數(shù)的算法
本人認為算法結構很重要,但語句結構也很重要,所以我喜歡一個算法給出多個語句結構……
最容易想到的傻瓜算法,逐個產(chǎn)生這些隨機數(shù),每產(chǎn)生一個,都跟前面的隨機數(shù)比較,如果重復,就重新產(chǎn)生:
算法一(1)
srand((unsigned)time(NULL));
for(j = 0; j < m; j++) {
a:a[j] = rand() % n;
for(i=0;i
很早的時候學編程都喜歡用goto語句,因為過去算法是用流程圖表示的,用goto可以直接轉譯,而且循環(huán)語句發(fā)展不完善,僅僅是作為條件分支的一個補充,如果條件分支箭頭向上就是一個循環(huán),而且goto可以實現(xiàn)過去循環(huán)所不能實現(xiàn)的結構,形成很巧妙的循環(huán)交叉。所以過去一般認為有兩種結構,順序和分支,循環(huán)是分支的特殊情況。但是break和continue語句使這些結構實現(xiàn)成為可能,后來發(fā)現(xiàn)goto的使用會造成維護和調試的許多麻煩,所以循環(huán)逐漸代替了goto,使程序更有層次?,F(xiàn)在編程不建議用goto了,如果用goto還會被認為編程修養(yǎng)不好……言歸正題,把它的goto去掉:
算法一(2)
srand((unsigned)time(NULL));
j=0;
while (j
{
a[j] = rand() % n ;
for(i=0;i
if(i
j++
}
?但是后來都建議用for循環(huán),這樣可以使循環(huán)條件跟緊湊:
算法一(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;}
?}
}
但這是個很笨的方法,且比較次數(shù)呈線性增長,越往后次數(shù)越多……另一個想法是生成一個就把它從中去掉,就可以實現(xià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[]是生成的隨機數(shù)列
?這樣做也太麻煩了,生成的直接做個標記不就行了?
算法三(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;
}
這個無循環(huán)體的while保證a[m]是初始化的0狀態(tài)。這生成了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;
}
但是總叫人不舒服,對這種算法誰有更好的寫法呢?
這種算法越到后面,遇到已使用過的元素的可能性越高,重復次數(shù)就越多,但是當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填滿整個數(shù)組;第三行,是隨機產(chǎn)生從0到n-2個數(shù)組下標,把這個下標的元素值跟n-1下標的元素值交換,一直進行到下標為1的元素。因此它只需要遍歷一次就能產(chǎn)生全部的隨機數(shù)。
但這樣會生成n個小于n的隨機數(shù),我們只要m個,再加兩句:
for(i=0;i<m;i++)
?b[i]=a[i];
b[]是生成的隨機數(shù)列
?如果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;
}
這樣可以萬無一失了……
希望對大家有幫助!

學習C/C++編程知識,提升C/C++編程能力,歡迎關注UP一起來成長!
另外,UP在主頁上傳了一些學習C/C++編程的視頻教程,有興趣或者正在學習的小伙伴一定要去看一看哦!會對你有幫助的~