C言語編 第54章 乱数

先頭へ戻る

この章の概要

この章の概要です。

乱数(疑似乱数)

乱数とは、規則性なく作られた数のことです。乱数を生成する方法を知っておくと、ランダムな部分を持ったプログラムが作れるようになります。例えば、現実世界でのサイコロのように、出た目に応じて処理を変えるようなことができます。

最初に「規則性なく~」と書きましたが、これは残念ながらコンピュータには難しいことです。通常、コンピュータが作り出す乱数は、特定の計算式を使って作り出したものです。例えば「x = a * B + C」のような計算式を用いて、B と C は固定、a だけを変化させます。計算結果の x を次回の a として使うようにして、繰り返し異なる値を生成していきます。a の初期値次第で、異なる乱数を生成できますが、a が同じ値であれば、いつも同じ結果にしかなりません。

このような乱数は、乱数のように見えるだけであって、実際には規則性があるため、擬似乱数と呼ばれています。

実行環境によっては、疑似乱数ではなく、真の乱数を得る方法が存在する可能性がありますが、C言語の標準機能としては存在しません。

擬似乱数を得るには、rand関数を使います。rand関数は、stdlib.h に以下のように宣言されています。

int rand(void);

この関数は、呼び出すたびに整数の乱数を返します。返される値の範囲は、0~RAND_MAX です。RAND_MAX は最低でも 32767 です。

呼び出されるたびに乱数を返すと言っても、擬似乱数ですから規則性があります。そのため、多くの回数、繰り返し呼び出し続ければ、いずれ同じ並びが現れます。

また、次のプログラムを何度も実行してみると分かりますが、そもそも、乱数が同じ順番でしか返ってきません。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int i;

    for( i = 0; i < 10; ++i ){
        printf( "%d\n", rand() );
    }

    return 0;
}

実行結果

41
18467
6334
26500
19169
15724
11478
29358
26962
24464

実行結果に並ぶ数値は、環境によって異なりますが、何度実行しても結果が変わることはありません。これが疑似乱数の特徴ですが、これでは実用的ではありません。そこで、毎回異なる乱数が得られるように対策を講じます。

シード値(乱数の種)

先ほどのサンプルプログラムのように、いつも同じ結果になってしまうのは、疑似乱数は特定の計算式で乱数を作るからです。「x = a * B + C」のような計算式を使っているとすれば、初回の a の値を変えない限り、いつも同じ結果しか生まれません。この a のように、生成される乱数の内容を決定付ける値を、シード値(乱数の種)と呼びます。

シード値は、特に指定しない限り、固定されたデフォルトの値が使われます。そのため、先ほどのサンプルプログラムでは、いつも同じ結果しか生み出せないのです。

シード値を設定するには、srand関数を使います。srand関数は stdlib.h に、以下のように宣言されています。

void srand(unsigned int seed);

引数には、設定したいシード値を指定します。また、srand関数は、最初の rand関数の呼び出しより前で呼び出しておきます。

srand関数を呼び出さずに rand関数を呼び出した場合、srand関数に 1 を渡したときと同じ結果になります。

試してみましょう。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int i;

    srand( 100 );
    
    for( i = 0; i < 10; ++i ){
        printf( "%d\n", rand() );
    }

    return 0;
}

実行結果

365
1216
5415
16704
24504
11254
24698
1702
23209
5629

シード値を設定できるといっても、srand関数の実引数がいつも同じ値では、結局同じ乱数しか生成できないことに変わりありません。「x = a * B + C」の初回の a が 100 に固定されているだけだからです。これでも、srand関数を呼ばなかったときとは異なる結果にはなりますが、望む結果はこういうことではないはずです。

プログラムを実行するたびに毎回異なる乱数が欲しければ、シード値も乱数でなければならないのです。これはなかなか難題のようですが、解決する方法があります。それは「時間」を利用することです。プログラムを起動した瞬間の時間は、恐らく実行のたびに異なると思われますから、これをシード値に使うのです。

実際に試してみましょう。現在の時間の取得には、第51章で登場した time関数を使います。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int i;

    srand( (unsigned int)time(NULL) );

    for( i = 0; i < 10; ++i ){
        printf( "%d\n", rand() );
    }

    return 0;
}

実行結果

24581
7509
3789
26783
4399
29529
32633
12466
5097
24208

このように、time関数の戻り値を乱数の種に利用します。こうすると、実行するたびに異なる結果になるはずです。

ただし、time関数が返す結果は秒単位にすぎないので、1秒以内に再実行すれば同じ結果になってしまいます。これは(時刻の設定が同じ)複数のコンピュータで一斉に同じプログラムを実行したら、すべてが同じ結果になるかも知れないことを意味しています。

なお、srand関数に渡す値が異なっていても、同じ順番の乱数が生成されることはあり得ます

乱数の利用

rand関数は、0~RAND_MAX の範囲の整数値を返しますが、これは普通、目的に合わないでしょう。例えば、サイコロのようなものを作りたいすれば、1~6 の範囲の値が欲しいはずです。

乱数は、目的に合うように加工して使うのが普通です。例えば、1桁の整数(0~9) が欲しいのであれば、10 で割った余りを使います。必要としている乱数の最小値が 0 であれば、これだけで済むので簡単です。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int i;

    srand( (unsigned int)time(NULL) );

    for( i = 0; i < 10; ++i ){
        printf( "%d\n", rand() % 10 );
    }

    return 0;
}

実行結果

3
8
8
7
5
4
6
1
3
3

サイコロの場合はどうでしょう。「目の数 - 1」で表すようにすれば、0~5 になるような加工を施せばいいのですが、不自然です。不自然な実装はバグを生みやすいので、素直に 1~6 を生成することを考えた方がいいでしょう。

こういう場合は、目的の範囲(1~6) を、一旦、最小値が 0 になるようにずらして考えてみます。つまり、1 減らして (0~5) の範囲を生成することを考えます。これは「rand() % 6」で生成できます。
目的の範囲はこれよりも 1 だけ大きいのですから、得られた結果を +1 します。したがって「rand() % 6 + 1」とすればよいと分かります。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    int i;

    srand( (unsigned int)time(NULL) );

    for( i = 0; i < 10; ++i ){
        printf( "%d\n", rand() % 6 + 1 );
    }

    return 0;
}

実行結果

6
1
6
1
6
4
3
4
1
1

実際のところ、これはあまり良い結果を生んでいません。実行結果を眺めると、2 と 5 の目が出ていないことが分かります。一方で、1 の目は 4回も出ています。これは、結果に偏りがあるということです。

rand関数が生成する乱数は、あまり質が高いものではありません。特に、このサンプルプログラムのように、単純に剰余を取る使い方をすると、同じような結果ばかりが並ぶことがあります。これは、一定の計算式で得られる値の、下位に近いビットが良い形に分散しないからです。

多少なりとも改善する方法はあるものの、そもそも rand関数の使用自体があまり好ましいものではなくなっています。特に、不正行為が起こり得る分野、セキュリティに絡むプログラムで乱数を使用する場合は、信頼性の高い方法をよく調査して下さい。残念ながら、C言語の標準機能には、rand関数以外の選択肢がありませんが、各環境が用意している方法があるかも知れません。


練習問題

問題① -10 ~ -1 の範囲の整数の乱数を生成するプログラムを作成して下さい。

問題② 'a'、'i'、'u'、'e'、'o' という5文字をランダムで選び出すプログラムを作成して下さい。

問題③ 0.0~1.0 の範囲の浮動小数点数の乱数を生成するプログラムを作成して下さい。


解答ページはこちら

参考リンク



更新履歴

'2018/5/3 新規作成。第51章に含まれていた内容を移動してきて、手直し。
乱数の利用」の項と、練習問題を新規で作成。



前の章へ(第53章 再帰呼び出し)

次の章へ(第55章 共用体)

C言語編のトップページへ

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS