[亂數] <細說> C/C++ 亂數基本使用與常見問題@ Edison.X. Blog

文章推薦指數: 80 %
投票人數:10人

注意,srand 正常而言一份程式碼(專案)只能執行一次,如果它放在for loop 裡,每次進行rand 前就用srand,會發現每次取出來的亂數是同一個數字。

3. 得知 ... Edison.X.Blog 跳到主文 YouLoveMe()?LetItBe():LetMeFree(); 部落格全站分類:數位生活 相簿 部落格 留言 名片 Jul22Sun201203:54 [亂數]C/C++亂數基本使用與常見問題   陸陸續續寫了EA 一、二年,以前亂數引導文回頭看時才發現,怎麼有這麼多細節的錯誤、沒系統。

這篇文章主要引導初學者使用亂數,同時附上常被翻出來討論的議題,C/C++適用,唯以C語言撰之。

也由於是引導初學者,所以在某些用詞上會較不正確, 像compiler、IDE會故意混為一談。

另外亂數原理也全都跳過。

另本文附程式碼,不附執行結果,有興趣自己跑一遍。

最後請注意本文在區間表達裡,開區間與閉區間 括號的使用,也就是, [a,b] , (a,b] ,[a,b), (a,b)  這四個表示的意義不同,     1.基本使用   C/C++之亂數函式放在stdlib.h/cstdlib裡面,在使用時直接呼叫rand()便可。

以下範例為產生5個亂數,並輸出。

CodeSnippet #include #include intmain() {     inti;     for(i=0;i<5;++i)         printf("%d",rand());     getchar();     return0; }     2.亂數種子   將上述的程式多執行幾次會發現,怎麼每次亂數產生的都一樣?原因是沒設亂數種子。

那什麼叫亂數種子? 原理我不講了,簡單的說產生器是一組公式,公式要給「初始值」。

再怎麼給亂數這組公式一個初始值?用srand()。

那初始值該給多少?初始值給固定的值都沒用,要會隨著環境變動的值才有意義, 像是記憶體使用量、processid、CPU使用率等,這些都是會隨環境變動, 但有些變動性可能不大,而最常用來給初始值的,是時間,所以上述程式改如下。

CodeSnippet #include #include #include intmain() {     inti;     unsignedseed;     seed=(unsigned)time(NULL);//取得時間序列     srand(seed);//以時間序列當亂數種子     for(i=0;i<5;++i)         printf("%d",rand());     getchar();     return0; } 而在srand那段,常常有人這麼寫   srand((unsigned)time(NULL));   這樣就不需暫存seed變數。

注意,srand正常而言一份程式碼(專案)只能執行一次,如果它放在forloop裡,每次進行rand前就用srand,會發現每次取出來的亂數是同一個數字。

  3.得知亂數最大值   後面會講為什麼要知道亂數最大值,這是一個重要的值。

C/C++提供的rand(),它有範圍限制,最小是0,最大是多少? 最大被定義在stdlib.h/cstdlib裡面的RAND_MAX,所以要得知最大是多少的話   CodeSnippet #include #include//RAND_MAX intmain() {     printf("%d\n",RAND_MAX);     getchar();     return0; }   目前可以確定的是,RAND_MAX至少會是32767,最大會是多少不一定。

但以筆者手邊的VisualC++2010環境而言,這個值是32767。

實際上VC6.0,VC2002/2003,VC2008,VC2010,gcc,Dev-C++,Code::Blocks(withmingw),這個值也都剛好是32767,只是他們實作的亂數細節不同而已。

至於日後其他改版會不會讓RAND_MAX更大?那就看那些軟體(compiler)如何實作了。

  4.產生固定範圍的整數亂數   我們以擲骰子為例,一個骰子有6個面,點數分別為1~6,要隨機擲一顆骰子怎麼做? 首先,1~6剛好有6個數字,所以可以這麼寫   result=rand()%6    %叫取模運算子,不懂的話回去翻書。

這樣下來可以確定,result只有{0,1,2,3,4,5}6種可能而已。

但實際上骰子的範圍是 1~6,而不是0~5,怎麼辦?很簡單,只要把結果+1就行了。

原本的結果是0~5,加1後結果變成1~6。

  總合以上說明,事實上我們可以給出一組公式,若要產生[low,up] 之整數亂數,我們可以這麼做   rand()%(up-low+1)+low    Q1:為什麼是%(up-low+1),而不是%(up-low)?A1:因low~up一共有(up-low+1)個數。

拿產生[1,6]來講,實際上共有6-1+1=6個數。

Q2:為什麼要加上low?A2:不加low的話實際上產生的是[0,up-low],加上low的話才是[low,up]。

  Ex1:模擬擲一顆骰子擲10次,並輸出其結果。

CodeSnippet #include #include #include//RAND_MAX intmain() {     inti;     srand((unsigned)time(NULL));     for(i=0;i<10;++i){         printf("%d",rand()%6+1);     }     getchar();     return0; } 有些數字可能不會出現,但多執行幾次應會出現,且範圍一定是1~6。

  Ex2:摸擬擲3顆骰子500次,紀錄點數和出現的次數,最後輸出每個點數共出現幾次。

3顆骰子點數最小為3,最大為18,所以輸出時只要判斷3~18出現的次數即可。

下面程式碼沒優化過,對初學者而言較易懂。

CodeSnippet #include #include #include//RAND_MAX intmain() {     intRunTimes=500  ;//測試次數     intSumTimes[20]={0};//紀錄點數出現的次數,全歸零     inti,sum,rnd;       //進行測試     for(i=0;i 產生浮點數亂數,通常都是先取得 [0,1)之浮點數亂數(可以包含零,但不包含1)。

這怎麼產生?還記得RAND_MAX是什麼意思吧?是rand()可能產生的最大值, 所以寫出這段碼出來。

  (double)rand()/(RAND_MAX+1.0);   Q1:為什麼要特別在rand()前面轉型成double?A1:簡單的說,我怕有人雞婆,把後面的1.0自己寫成1,這時候不加上(double)的話結果除出來一定是0;若後面的1.0都不動它的話,前面的double可以拿掉無誤。

Q2: 那為什麼分母還要特別加上1.0?A2 :前面有說過了,rand()最大值可到RAND_MAX,不加上1.0的話會使得(double)rand()/RAND_MAX,結果有機率變成1,但這與我的前提:不包含1是相違的。

Q3:那除了加上1.0這數字外,可以改成加其他數字啊!諸如2.0,100.0,10000.0之類的。

A3:又如我剛剛所說,是要產生[0,1)之間的浮點數亂數,假定RAND_MAX=32767,如果加上10000.0的話,這個結果最大值會變成了32767/(32767+10000)=0.247,明顯[0.25,1.0)都沒機會生成了。

但如果改成0.5,之類,小於1較大的小數,到是可接受,不過這種數字幾乎沒人在用。

  那,產生出[low,up)之浮點數隨機亂數(不含up)怎做? 剛剛已給出了rndf=[0,1)之公式,所以要擴展到[low,up)時,只要做點修改就行, 概念是[low,up)亂數,等於(low~up距離)*([0,1)亂數)+(下限low)。

  doublelow=5.1,up=7.3,rndf,result; rndf=(double)rand()/(RAND_MAX+1.0);//產生[0,1)浮點亂數 result=(up-low)*rndf+low; //產生[low,up)浮點亂數   寫成一行型式   doublelow=5.1,up=7.3,result; result=(up-low)*rand()/(RAND_MAX+1.0)+low;   Q4:上面範例都是在討論不含上界的情況,如果要含上界的話呢?A4:很簡單,把上面的RAND_MAX+1.0部份,全都改成RAND_MAX即可,這樣就有機會出現上界。

  6.再談整數亂數   再回到擲骰子的問題上,要產生[1,6]之間的整數亂數,事實上有另一種方法,就是先產生[1,7)的浮點數亂數,之後再強制轉型成整數,所以程式碼如下所示。

  CodeSnippet #include #include #include intmain() {     intresult;     doubler01,rnd;       //亂數種子     srand((unsigned)time(NULL));     //產生[0,1)之亂數     r01=(double)(rand())/(RAND_MAX+1.0);     //產生[1,7)之亂數     rnd=r01*(7.0-1.0)+1.0;     //強制轉型給result     result=(int)(rnd);     printf("result=%d\n",result);//輸出結果     getchar();     return0; }   Q1:為什麼是產生[1,7)之浮點數亂數,而不是產生[1,6]之浮點數亂數?A1:重點在後半段還要強制轉型。

若一開始就產生[1,6]之浮點亂數時,要使得轉型後結果為6只有一種條件可達成:rand()必須是RAND_MAX。

這部份原理很簡單,但建議自己想想比較有收獲。

  根據以上之敘述,觀查可納出一結論:當要產生出[low,up]之整數亂數時,可有另一種方式, 便是產生[low,up+1)之浮點數亂數後,再進行強制轉型成整數。

如下。

  CodeSnippet intlow=-5,up=10;//上下限 intresult;//結果 doubler01,r; r01=(double)rand()/(RAND_MAX+1.0);//產生[0,1)浮點亂數 r=r01*(up-low+1.0)+low;//產生[low,up+1)浮點亂數 result=(int)r;  //最後強制轉型。

  甚至可包成副函式或寫成一行。

  CodeSnippet //產生[low,up]之隨機整數亂數 intrand_int(intlow,intup) {     return(int)((rand()/(RAND_MAX+1.0))*(up-low+1.0)+low); }   如果是要產生[low,up)之隨機整數亂數的話呢?這在做陣列索引很常見, 因陣列有N個元素,範圍只能是[0,N),而不能是[0,N]。

實際上產生[low,up)之整數亂數,就是產生[low,up-1]之整數亂數, 一個方法是直接以rand_int(low,up-1)方式代入上式; 硬要從函式裡面改的話,就是先產生[low,up)之浮點亂數後, 再強制轉型成整數資料型態。

  CodeSnippet //產生[low,up)之隨機亂數 intrand_int2(intlow,intup) {     return(int)((rand()/(RAND_MAX+1.0))*(up-low)+low); }   接下來可以認真討論,為什麼大多數較不建議用取模運算子(mod,%)來求浮點亂數了。

我們先假設一種情況,若某個亂數產生器,他的RAND_MAX=13,目前要產生[0,3]之整數亂數。

  以取模運算子撰之,rst=rand()%4,看一下數值分佈的情況。

  rand()=0,4,8,12 :rst=0rand()=1,5,9,13 :rst=1rand()=2,6,10   :rst=2rand()=3,7,11   :rst=3    所以rst=2與rst=3出現的機率比較低, 機率比較低的rst,都被安排到rst可能出現之值的後半段。

  再考慮  rst=(int)((rand()/(RAND_MAX+1.0))*(up-low+1.0)+low); rst=(int)(rand()/14.0*4); 用乘、除法的情況   rand()=0,1,2,3  :rst=0rand()=4,5,6    :rst=1rand()=7,8,9,10 :rst=2rand()=11,12,13 :rst=3    所以rst=1和rst=3出現的機率比較低, 機率比較低的rst,都被均勻打散到rst可能出現之值範圍內。

鑑於亂數應符合均勻之特性,故較多人建議別用取模(mod)方式取整數亂數。

  一樣的議題,若欲產生的整數亂數範圍超過RAND_MAX時,這種方法也是有些數字沒辦法產生到。

只是這種方法沒辦法產生的數字,是被打散到各區塊裡,而不是像取模運算子全擠在後半段。

總之就是建議要額外處理。

  7. 不均勻亂數問題   現假設一種情況是,希望不是每個數出現的機率都一樣,假設有4個數, 1出現機率為0.4; 2出現機率為0.1; 3出現機率為0.3; 4出現機率為0.2; 怎麼做?   針對這種較簡單的機率數字,1234出現的比率為4:1:3:2,加總為10, 所以有種做法如下 (1)開大小為10的陣列Arr[10], (2)依序填入4個1、1個2、3個3、2個4。

(3)隨機產生[0,9]之整數亂數,pos,再取得Arr[pos]出來即可。

概念上之程式碼約如下述。

  CodeSnippet #include #include #include   intmain() {     inti,j,pos;     intArr[10];//開大小為10的陣列Arr[10]          //  依序填入4個1、1個2、3個3、2個4   Arr[0]=Arr[1]=Arr[2]=Arr[3]=1;//4個1     Arr[4]=2;//1個2     Arr[5]=Arr[6]=Arr[7]=3;//3個3     Arr[8]=Arr[9]=4;//2個4       srand((unsigned)time(NULL));     //隨機產生[0,9]之整數亂數,pos,再取得Arr[pos]出來     for(i=0;i<10;++i){//取10次         //產生[0,9]整數亂數pos         pos=(int)(rand()/(RAND_MAX+1.0)*10);         //取出Array[pos]         printf("%d",Arr[pos]);     }     getchar();     return0; }   [HomeWork]依上述的數字出現之機率,做10萬次測試,最後真正實際上1,2,3,4出現之次數、機率為何?是否接近於當初設定之機率?   試再想另一種情形,若  10出現機率為0.123,20出現機率為 0.234, 30出現機率為0.345,40出現機率為 0.298, 照上面的方法,不就要設一個大小為1000的陣列了嗎? 那如果小數點後面加到10位數,不就要設一個大小為10^10的陣列了? 這個記憶體根本就放不下。

  另一種方式是用累計機率,我們先做累計機率的表出來   [0]10:機率=0.123,累計機率=0.123,令為CP[0] [1]20:機率=0.234,累計機率=0.123+0.234=0.357,令為CP[1] [2]30:機率=0.345,累計機率=0.357+0.345=0.702,令為CP[2] [3]40:機率=0.298,累計機率=0.702+0.298=1.000,令為CP[3]   累計機率算出來之後,我們只需要產生[0,1)之随機浮點數亂數rndf, 去檢查rndf落在哪段區間,rndf #include #include   intmain() {     inti,pos,n=4;//4個元素     intNum[4]={10,20,30,40};//欲出現之數字     doubleProb[4]={0.123,0.234,0.345,0.298};//數字對應之出現機率     doubleCP[4];     doublerf;//隨機機率       srand((unsigned)time(NULL));     //step1:做累計機率計算     CP[0]=Prob[0];     for(i=1;i   要產生20個[1,100]不重覆之亂數,怎麼做? 一種作法是先開大小為20的陣列Arr[20],每產生一個亂數的時候,就到Arr裡面看有沒有重覆,如果沒有重覆才加進去,有重覆的話就再取下一個亂數。

示例碼如下。

  CodeSnippet #include #include #include   intmain() {     intn=20;//找20個相異亂數     inti,cnt,num,Arr[20];     intfind;   srand((unsigned)time(NULL));     cnt=0;//已有不重覆亂數之個數     while(cnt   回到最初的問題,從[1,100]裡挑出20個不重覆之亂數,結果填到Array裡。

這裡我們先為這些數字做點符號定義表示。

從[low,up]裡,挑出n個不重覆之亂數,結果填到Array裡。

洗牌(shuffle)法的概念是,剛剛的[low,up],每個數字都視為撲克牌裡的一張牌,所以這副撲克牌共有(up-low+1)張,於是開陣列Poker[up-low+1],並填入1:100。

再來是模擬洗牌的過程,洗牌方式非常非常多!第一種是,隨機抽出第pos1張,再隨機抽出第pos2張,再將這兩張牌交換。

進行low-up+1(100)次。

整個動作做完後,再把poker前面的20(n,欲取幾個亂數)張牌,放到Arr裡面,就是答案了。

看碼最清楚。

CodeSnippet #include #include #include   voidshuffle_1(int*arr,intn,intlow,intup) {     inti,pos1,pos2,tmp;     intSize=up-low+1;//整份poker大小       //配置一份poker[Size]     int*Poker=(int*)malloc(sizeof(int)*Size);     for(i=0;i0;--j)注意,判別式裡沒有等於零。

(2)取整數亂數pos,範圍為[0,j],交換poker[j],poker[pos] 程式碼約如下述。

CodeSnippet voidKnuthShuffle(int*arr,intn,intlow,intup) {     inti,pos1,pos2,tmp;     intSize=up-low+1;//整份poker大小       //配置一份poker[Size]     int*Poker=(int*)malloc(sizeof(int)*Size);     for(i=0;i0;--i){         //隨機取出[0,i]之poker         pos=(int)(rand()/(RAND_MAX+1.0)*(i+1));         //交換這兩張牌         tmp=Poker[pos];         Poker[pos]=Poker[i];         Poker[i]=tmp;     }     //洗完牌,前面的n張再給Arr     for(i=0;i   再續上個問題,從[1,100]裡挑出20個不重覆之亂數,結果填到Array裡。

1~100有100個元素,排序法方式是直接開兩個陣列:intRst[100],intRnd[100], Rst[100]從1填到100,Rnd[100]是連續取100個亂數填進去, 填完之後,對Rnd做排序,而在排序過程中有用到交換,Swap(Rnd[i],Rnd[j]) 交換時連Rst也一起交換Swap(Rst[i],Rst[j]),程式碼約如下述。

  CodeSnippet #include #include #include   #defineSWAP(a,b){intt=a;a=b;b=t;} voidSortShuffle(int*arr,intn,intlow,intup) {     inti,j;     intSize=(up-low+1);     int*Rst=(int*)malloc(sizeof(int)*Size);     int*Rnd=(int*)malloc(sizeof(int)*Size);       if(Rst==NULL||Rnd==NULL)return;       for(i=0;iRnd[j]){                 //交換時連Rst也一起交換                 SWAP(Rnd[i],Rnd[j]);                 SWAP(Rst[i],Rst[j]);             }         }     }     //Rst前n筆存入arr     for(i=0;i=1012 ,忽略1所帶來之影響,兩邊取log10log10(2n)>=12,nlog10(2)>=12n>=12/log10(2)=39.86 由於n必為大於等於1之整數,故取40。

  step3:由bit數產生亂數 在假設RAND_MAX=32767之情況下,取一次rand()有15bits,故要到40bits至少要取3次才可達到。

但以筆者手邊環境而言,int/unsignedint只有32bits,沒辦法達到40bits之要求,故改用資料型態unsignedlonglong(更好的做法是用uint64_t)去存結果,下面是一種作法。

  CodeSnippet typedefunsignedlonglongu64;       //typedef u64rst=((u64)(rand())<<25)|   //bit[39:25]     (u64)(rand())<<10)|           //bit[24:10]     (u64)(rand()&0x3ffULL);        //bit[9:0]   切割方式為15+15+ 10=40bits,左移bits數依序為[15+10, 10,0]。

 但考慮到高位元之循環率較低位元循環率小,所以將40切割成14+13+13,且取出時取高bits為主,依序應該左移bits數為 [13+13,13,0]。

  CodeSnippet typedefunsignedlonglongu64;//typedef u64rst=(\    ((u64)(rand()>>1)<<26)|//high14,Lshift26bits    ((u64)(rand()>>2)<<13)|//high13,Lshift13bits    ((u64)(rand()>>2)       ));//high13,Lshift0bits    結束之後,這只能產生[0,240-1]之亂數產生器,要再產生[0,1]之浮點亂數,就再除上240-1。

  CodeSnippet typedefunsignedlonglongu64;//typedef   u64rand40(){     return (\         ((u64)(rand()>>1)<<26)|//high14,Lshift26bits         ((u64)(rand()>>2)<<13)|//high13,Lshift13bits         ((u64)(rand()>>2)       ));//high13,Lshift0bits }   doublerandf40(){     constu64NEW_RAND_MAX= (1ULL<<40)-1ULL;     return(double)rand40()/NEW_RAND_MAX; }    大亂數問題至此結束。

提醒,一般簡單統計用的亂數可以用此法產生沒錯(像一些演化式演算法,或蒙地卡羅演算法),若用於加解密等,通常不會再用rand()方式進行亂數產生。

    13.其他   亂數其他議題相當多,有些也不好實作出來,本篇所提是較為基礎之部份,其他諸如蒙地卡邏MAMC、其他亂數分佈等議題,便不於此文探討。

全站熱搜 創作者介紹 edisonx Edison.X.Blog edisonx發表在痞客邦留言(10)人氣() E-mail轉寄 全站分類:校園生活個人分類:亂數此分類上一篇:[C&++]任意亂數分佈產生器(?) 上一篇:[C語言數值分析]ieee754欄位分析加速mathlibrary 下一篇:[C&++]Note-實用議題 ▲top 留言列表 發表留言 站方公告 [公告]2022年度農曆春節期間服務公告[公告]MIB廣告分潤計劃、PIXwallet錢包帳戶條款異動通知[公告]2021年度農曆春節期間服務公告 活動快報 留言旅行地點領禮物 按讚並追蹤PIXstyleMe,於此貼文的留言處寫下「... 看更多活動好康 我的好友 熱門文章 文章分類 開發手札(2) 未實作的想法(4)心得筆記(2) C/C++(8) C/C++Note(52)亂數(10)Debug(9)HiddenFeaturesinC(6)面試題庫(12)OONOTE(0)C/C++FAQ(4)STLNote(3) 應用軟體/工具(1) Office(1) 數值分析(9) 非線性方程式求解(10)矩陣運算(7)深入質數(5)浮點數(9)複數Complex(2)積分法(2)多項式內差法(2)常見關於數(5)math.h/cmathapplication(8) 程式之美(1) 遊戲之樂(3) VB.Net(1) VB.NetNote(1) 英文(1) 專題單字(1) AutoIt!!(2) AutoIt!!Note(13)Auto-Dll(9) VBA(4) VBANote(9)VBAFAQ(5)VBAtec.(1)VBA_Note2(3) MFC(1) MFC雜記(4) Win32(8) Win32-Console(5)Process(10)檔案系統(1)音效(1)隱喻外掛(3)GDI(2)Systemundoc.(0)記憶體管理(1) 環境與Script(5) visualstudio(6)批次檔batch(2)VBS(0)程式環境架構(3)Library(3) 數學整理(2) 有趣數學(2)常用公式(1) 演算法(7) AI(13)Bit-Hacks(1)大數(5)資料結構(0)影像(2)遞迴-recursive(2)回溯.列舉.遞迴(2) 程設亂語(1) 胡言亂語(11) SmallTalk(1) SmallTalk(27) 最新文章 最新留言 動態訂閱 文章精選 文章精選 2017二月(1) 2016十月(1) 2014四月(1) 2014三月(1) 2014二月(2) 2014一月(1) 2013九月(2) 2013五月(1) 2013一月(3) 2012十二月(10) 2012十一月(6) 2012十月(4) 2012八月(1) 2012七月(13) 2012六月(17) 2012五月(2) 2012四月(15) 2012三月(9) 2012一月(4) 2011十二月(7) 2011十一月(33) 2011十月(8) 2011九月(8) 2011八月(11) 2011七月(3) 2011六月(7) 2011五月(12) 2011四月(13) 2011三月(5) 2011二月(1) 2011一月(31) 2010十二月(34) 2010十一月(22) 2008六月(1) 2008五月(1) 2008三月(11) 2007十一月(1) 2005十一月(2) 所有文章列表 文章搜尋 新聞交換(RSS) 誰來我家 參觀人氣 本日人氣: 累積人氣: QRCode POWEREDBY (登入) {{article.user_name}} {{article.timestamp*1000|date:'MMM.dd.y.hh.mm'}} {{article.title}} {{article.content}} 我要留言 回到頁首 回到主文 免費註冊 客服中心 痞客邦首頁 ©2003-2022PIXNET 關閉視窗 PIXNET Facebook Yahoo! Google MSN {{guestName}} (登出) 您尚未登入,將以訪客身份留言。

亦可以上方服務帳號登入留言 請輸入暱稱(最多顯示6個中文字元) 請輸入標題(最多顯示9個中文字元) 請輸入內容(最多140個中文字元) 請輸入左方認證碼: 看不懂,換張圖 請輸入驗證碼 送出留言



請為這篇文章評分?