乱数の発生
今回は乱数を発生させるライブラリ関数について説明します。
乱数とは、例えばサイコロを振った時に出る目や、よく切ってあるトランプのカードの山から適当に選んだ1枚のカードのような
ランダムな数値のことをいいます。
よって乱数を発生させる関数とは、ある値の範囲内において一様なランダムな数値を返す関数rand()の事をいいます。
一様というのはどの値もほぼ同じ確率で出ることを意味します。
例えばサイコロを振った場合、ある目だけ出やすいなんて事があったら、それはイカサマですよね。
だから一様であることが必要になります。
しかしランダムな数値といっても、ソフトウェア的に完璧なランダムな値を返せることはほぼ不可能であるため、
見た目にはランダムに見えるのですが、実際のところではある規則性に基づいて擬似的な乱数を返すようになっています。
では記述の方法について説明します。
(例1)
#include <stdio.h> int main(){ int a; a = rand()%6 + 1; /*1から6の間の値をint型で返す*/ printf("結果: %d\n",a); } |
このようにrand()関数を記述し、実行すれば0から5の間でランダムな値が出力されます。
rand()関数の内部について見ていきましょう。
rand()は0からRAND_MAX(コンパイラによって異なる大きな値)をint型で返します。
しかしサイコロを振るように出て欲しい値の範囲が決まってる場合(6面体のサイコロだと1〜6)は割り算の余り(%)や加算を利用します。
上の記述ではrand()から得られた0〜RAND_MAXまでのランダムな値を6で割ることによって、その余りが0〜5のいずれかになります。
で、さらに1を加えることで1〜6のランダムな値が返すことができるという仕組みです。
よって出て欲しい乱数の範囲が0からkまでの場合は
rand() % (k+1);
とします。その他の場合においては(例1)のように割り算の余りや加算を使えば表せるせるのでやってみてください。
しかし何度も(例1)のプログラムを実行していれば気づくと思いますが、実行を繰り返すたびに同じ値が出力されているはずです。
それじゃ乱数もへったくれもありませんねw 実はこのプログラムはまだ未完成なのです。
何故そうなるのかというと、よく切ったトランプの山を裏返しにして一番上のカードを抜いて、またもとの位置に戻すような事を想像してみてください。これでは何度引いても同じ値しか出ませんね。
そこでプログラム(カード抜く作業)を一度終了したら、初期化(カードを切りなおす)作業をする必要があります。
では乱数を初期化の関数srand()を使った(例2)を見てください。
(例2)
#include <stdio.h> #include <stdlib.h> /*忘れないように*/ #include <time.h> /*忘れないように*/ int main(){ int a; srand((unsigned)time(NULL)); /*乱数の初期化*/ a=rand()%6; printf("結果: %d\n",a); } |
乱数を初期化する関数srand()はstdlib.hに入ってるので、stdlib.hをインクルードしておいてください。
srand()の引数の中が重要なのです。ここになにも書かれてないと初期化の意味がありません。
定数ではない、ばらばらな値を持たせる事が必要なのです。
(例2)の場合にはtime(NULL)と書いてあります。あまり私も詳しくは分かりませんので、説明を詳しくはできませんが、time()関数というのは秒単位で時間をカウントしているものだと思ってみてください。
だから1秒後にはsrand()の引数が変化するようになります。
これによってsrand()はばらばらな値を持つことができるので、実行するごとにrand()に違う値を返せるようになります。
time()関数を使用するためにはtime.hをインクルードしてください。
ただしtime関数を使ったsrand()には弱点があります。
できる方は(例2)のプログラムを1秒未満に2回実行してみてください。たぶん同じ値が出力されるはずです。
それは実行間隔が1秒未満だとsrand((unsigned)time(NULL));
の引数が変化しないためです。
time()関数はコンマ何秒という時間は計測できません。
しかし大きなプログラムを書いた場合には、time関数を使ったsrand()の弱点は気にする必要がありません。
なぜならプログラムを実行している間に1秒経ってしまうからです。
これ以外にもsrand()の引数にばらばらな値をとらせる方法がいくつかあるらしいのですが、私には分かりませんw
あとsrand()はプログラムの最初のほうに一回書いておけば初期化することができますので、何度も書く必要はありません。
ではちょっと練習がてら以下のプログラムを追っかけてみてください。
(例3)
#include <stdio.h> #include <stdlib.h> #include <time.h> int main(){ int a[5],big=0,n; srand((unsigned)time(NULL)); /*乱数の初期化*/ for(n=0;n<5;n++){ a[n]=rand()%10 + 1; /*1〜10の乱数を返す*/ if (a[n]>big) /*出た乱数が一番大きな値なら*/ big=a[n]; } printf("\n配列の要素: "); /*出力作業*/ for(n=0;n<5;n++) printf("%d ",a[n]); printf("\n\n最大値: %d\n",big); } |
このプログラムは配列a[]の各要素に1から10のランダムな値を入れていき、各要素野中で一番大きな値を出力するプログラムです。
今回やってきたことと、配列について知識があれば簡単ですね。
(問い)
ではここで問題です。
乱数を発生させる関数rand()は正の値のみしか返せません。
そこで正負の値どちらでも返せるようなプログラムをrand()関数を用いて作成してみてください。
乱数の範囲は-30から+30まででお願いします。
rand()関数を2つ以上使っても構いません。
回答例は後ほど発表します。