【C言語】重複しない乱数の生成方法

C言語で乱数を生成する方法を知りたいですか?この記事では、基本的な乱数生成方法から、重複しない乱数を生成するための具体的な方法までをわかりやすく解説します。

配列、リスト、ハッシュセットを使った方法を紹介し、それぞれのパフォーマンスも比較します。

さらに、ゲームや抽選システムなどの実際の応用例も取り上げます。

初心者の方でも理解できるように、サンプルコードとともに丁寧に説明していますので、ぜひ参考にしてください。

目次から探す

基本的な乱数生成方法

C言語で乱数を生成するためには、標準ライブラリに含まれるrand()関数srand()関数を使用します。

ここでは、これらの関数の基本的な使い方と、乱数の範囲を指定する方法について解説します。

rand()関数の使い方

rand()関数は、0からRAND_MAXまでの整数をランダムに生成する関数です。

RAND_MAXは標準ライブラリで定義されている定数で、通常は32767です。

以下にrand()関数の基本的な使い方を示します。

#include <stdio.h>
#include <stdlib.h>
int main() {
    int random_number;
    // 乱数を生成
    random_number = rand();
    // 生成された乱数を表示
    printf("Generated random number: %d\n", random_number);
    return 0;
}

このプログラムを実行すると、0からRAND_MAXまでのランダムな整数が表示されます。

srand()関数でシード値を設定する

rand()関数は、プログラムを実行するたびに同じ順序の乱数を生成します。

これを防ぐために、srand()関数を使ってシード値を設定します。

シード値を設定することで、異なる乱数列を生成することができます。

通常、シード値には現在の時刻を使用します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    int random_number;
    // 現在の時刻をシード値として設定
    srand(time(NULL));
    // 乱数を生成
    random_number = rand();
    // 生成された乱数を表示
    printf("Generated random number: %d\n", random_number);
    return 0;
}

このプログラムを実行するたびに、異なる乱数が生成されるようになります。

乱数の範囲を指定する方法

rand()関数が生成する乱数は0からRAND_MAXまでですが、特定の範囲内の乱数を生成したい場合があります。

例えば、1から100までの乱数を生成する場合、以下のようにします。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    int random_number;
    int lower = 1;  // 乱数の下限
    int upper = 100;  // 乱数の上限
    // 現在の時刻をシード値として設定
    srand(time(NULL));
    // 1から100までの乱数を生成
    random_number = (rand() % (upper - lower + 1)) + lower;
    // 生成された乱数を表示
    printf("Generated random number between %d and %d: %d\n", lower, upper, random_number);
    return 0;
}

このプログラムでは、rand()関数の結果をupper - lower + 1で割った余りにlowerを足すことで、指定した範囲内の乱数を生成しています。

以上が、C言語での基本的な乱数生成方法です。

次のセクションでは、重複しない乱数を生成する方法について詳しく解説します。

重複しない乱数を生成する方法

C言語で重複しない乱数を生成する方法はいくつかあります。

ここでは、配列、リスト、ハッシュセットを使った方法を紹介します。

配列を使った方法

配列を使った方法は、まず配列を初期化し、その後シャッフルしてから乱数を取得する方法です。

配列の初期化

まず、配列を初期化します。

例えば、1から10までの重複しない乱数を生成する場合、以下のように配列を初期化します。

#include <stdio.h>
int main() {
    int arr[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = i + 1;
    }
    // 配列の内容を表示
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

配列のシャッフル

次に、配列をシャッフルします。

Fisher-Yatesアルゴリズムを使うと効率的にシャッフルできます。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void shuffle(int *array, int n) {
    srand(time(NULL));
    for (int i = n - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}
int main() {
    int arr[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = i + 1;
    }
    shuffle(arr, 10);
    // シャッフル後の配列の内容を表示
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

シャッフルされた配列から乱数を取得

シャッフルされた配列から順に値を取得することで、重複しない乱数を得ることができます。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void shuffle(int *array, int n) {
    srand(time(NULL));
    for (int i = n - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}
int main() {
    int arr[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = i + 1;
    }
    shuffle(arr, 10);
    // シャッフル後の配列から乱数を取得
    for (int i = 0; i < 10; i++) {
        printf("Random number %d: %d\n", i + 1, arr[i]);
    }
    return 0;
}

リストを使った方法

リストを使った方法は、リストからランダムに要素を選択し、選択された要素をリストから削除する方法です。

リストの初期化

まず、リストを初期化します。

ここでは、シンプルな配列をリストとして扱います。

#include <stdio.h>
int main() {
    int list[10];
    for (int i = 0; i < 10; i++) {
        list[i] = i + 1;
    }
    // リストの内容を表示
    for (int i = 0; i < 10; i++) {
        printf("%d ", list[i]);
    }
    return 0;
}

リストからランダムに要素を選択

次に、リストからランダムに要素を選択します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    int list[10];
    for (int i = 0; i < 10; i++) {
        list[i] = i + 1;
    }
    srand(time(NULL));
    int index = rand() % 10;
    int random_number = list[index];
    printf("Random number: %d\n", random_number);
    return 0;
}

選択された要素の削除

選択された要素をリストから削除し、リストを詰めます。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    int list[10];
    for (int i = 0; i < 10; i++) {
        list[i] = i + 1;
    }
    srand(time(NULL));
    for (int i = 10; i > 0; i--) {
        int index = rand() % i;
        int random_number = list[index];
        printf("Random number: %d\n", random_number);
        // 選択された要素を削除し、リストを詰める
        for (int j = index; j < i - 1; j++) {
            list[j] = list[j + 1];
        }
    }
    return 0;
}

ハッシュセットを使った方法

ハッシュセットを使った方法は、ハッシュセットに乱数を追加し、重複を避けるためのチェックを行う方法です。

ハッシュセットの初期化

まず、ハッシュセットを初期化します。

C言語には標準でハッシュセットがないため、ここでは簡単なビットマップを使います。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX_NUM 10
int main() {
    int bitmap[MAX_NUM] = {0};
    // ビットマップの内容を表示
    for (int i = 0; i < MAX_NUM; i++) {
        printf("%d ", bitmap[i]);
    }
    return 0;
}

ハッシュセットに乱数を追加

次に、ハッシュセットに乱数を追加します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX_NUM 10
int main() {
    int bitmap[MAX_NUM] = {0};
    srand(time(NULL));
    for (int i = 0; i < MAX_NUM; i++) {
        int random_number = rand() % MAX_NUM;
        bitmap[random_number] = 1;
    }
    // ビットマップの内容を表示
    for (int i = 0; i < MAX_NUM; i++) {
        printf("%d ", bitmap[i]);
    }
    return 0;
}

重複を避けるためのチェック

重複を避けるために、既に存在する乱数をチェックし、存在しない場合のみ追加します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX_NUM 10
int main() {
    int bitmap[MAX_NUM] = {0};
    srand(time(NULL));
    int count = 0;
    while (count < MAX_NUM) {
        int random_number = rand() % MAX_NUM;
        if (bitmap[random_number] == 0) {
            bitmap[random_number] = 1;
            printf("Random number: %d\n", random_number + 1);
            count++;
        }
    }
    return 0;
}

以上が、C言語で重複しない乱数を生成する方法です。

配列、リスト、ハッシュセットを使った方法それぞれにメリットとデメリットがありますので、用途に応じて適切な方法を選んでください。

パフォーマンスの比較

重複しない乱数を生成する方法にはいくつかのアプローチがありますが、それぞれの方法にはパフォーマンスの違いがあります。

ここでは、配列方式、リスト方式、ハッシュセット方式のパフォーマンスを比較してみましょう。

配列方式のパフォーマンス

配列方式では、まず配列を初期化し、その後シャッフルを行います。

この方法のパフォーマンスは、配列のサイズに依存します。

配列の初期化とシャッフルの両方がO(n)の時間複雑度を持つため、全体の時間複雑度はO(n)となります。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void shuffle(int *array, int n) {
    for (int i = n - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}
int main() {
    int n = 10;
    int array[n];
    for (int i = 0; i < n; i++) {
        array[i] = i + 1;
    }
    srand(time(NULL));
    shuffle(array, n);
    for (int i = 0; i < n; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
    return 0;
}

このコードでは、配列を初期化し、シャッフルしてから出力しています。

配列のサイズが大きくなると、シャッフルにかかる時間も増加します。

リスト方式のパフォーマンス

リスト方式では、リストからランダムに要素を選択し、その要素をリストから削除します。

この方法のパフォーマンスは、リストのサイズに依存します。

要素の選択と削除がO(n)の時間複雑度を持つため、全体の時間複雑度はO(n^2)となります。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef struct Node {
    int data;
    struct Node *next;
} Node;
Node* createList(int n) {
    Node *head = NULL;
    Node *temp = NULL;
    for (int i = 1; i <= n; i++) {
        Node *newNode = (Node*)malloc(sizeof(Node));
        newNode->data = i;
        newNode->next = NULL;
        if (head == NULL) {
            head = newNode;
            temp = head;
        } else {
            temp->next = newNode;
            temp = temp->next;
        }
    }
    return head;
}
int getRandomElement(Node **head, int n) {
    int index = rand() % n;
    Node *temp = *head;
    Node *prev = NULL;
    for (int i = 0; i < index; i++) {
        prev = temp;
        temp = temp->next;
    }
    int data = temp->data;
    if (prev == NULL) {
        *head = temp->next;
    } else {
        prev->next = temp->next;
    }
    free(temp);
    return data;
}
int main() {
    int n = 10;
    Node *list = createList(n);
    srand(time(NULL));
    for (int i = 0; i < n; i++) {
        int randomElement = getRandomElement(&list, n - i);
        printf("%d ", randomElement);
    }
    printf("\n");
    return 0;
}

このコードでは、リストを初期化し、ランダムに要素を選択してから出力しています。

リストのサイズが大きくなると、要素の選択と削除にかかる時間も増加します。

ハッシュセット方式のパフォーマンス

ハッシュセット方式では、ハッシュセットに乱数を追加し、重複を避けるためのチェックを行います。

この方法のパフォーマンスは、ハッシュセットの操作がO(1)の時間複雑度を持つため、全体の時間複雑度はO(n)となります。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdbool.h>
#define TABLE_SIZE 100
typedef struct HashSet {
    int table[TABLE_SIZE];
    bool occupied[TABLE_SIZE];
} HashSet;
void initHashSet(HashSet *set) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        set->occupied[i] = false;
    }
}
bool add(HashSet *set, int value) {
    int index = value % TABLE_SIZE;
    while (set->occupied[index]) {
        if (set->table[index] == value) {
            return false;
        }
        index = (index + 1) % TABLE_SIZE;
    }
    set->table[index] = value;
    set->occupied[index] = true;
    return true;
}
int main() {
    int n = 10;
    HashSet set;
    initHashSet(&set);
    srand(time(NULL));
    for (int i = 0; i < n; i++) {
        int randomValue;
        do {
            randomValue = rand() % 100;
        } while (!add(&set, randomValue));
        printf("%d ", randomValue);
    }
    printf("\n");
    return 0;
}

このコードでは、ハッシュセットを初期化し、重複しない乱数を生成してから出力しています。

ハッシュセットの操作が効率的であるため、大規模なデータセットでも高速に動作します。

以上のように、配列方式、リスト方式、ハッシュセット方式のそれぞれに特徴があります。

用途やデータの規模に応じて最適な方法を選択することが重要です。

応用例

ゲームの乱数生成

ゲーム開発において、重複しない乱数の生成は非常に重要です。

例えば、カードゲームでは、デッキからカードを引く際に同じカードが複数回出現しないようにする必要があります。

以下に、カードゲームのデッキシャッフルを例にしたコードを示します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define DECK_SIZE 52
void shuffle(int *deck, int size) {
    for (int i = size - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        int temp = deck[i];
        deck[i] = deck[j];
        deck[j] = temp;
    }
}
int main() {
    int deck[DECK_SIZE];
    for (int i = 0; i < DECK_SIZE; i++) {
        deck[i] = i + 1; // カード番号を1から52まで設定
    }
    srand(time(NULL)); // シード値を現在の時間で設定
    shuffle(deck, DECK_SIZE);
    printf("シャッフルされたデッキ:\n");
    for (int i = 0; i < DECK_SIZE; i++) {
        printf("%d ", deck[i]);
    }
    printf("\n");
    return 0;
}

このコードでは、shuffle関数を使ってデッキをシャッフルしています。

rand()関数を使ってランダムなインデックスを生成し、カードの位置を入れ替えています。

抽選システム

抽選システムでも重複しない乱数の生成が求められます。

例えば、抽選で当選者を選ぶ際に、同じ人が複数回当選しないようにする必要があります。

以下に、抽選システムの例を示します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define NUM_PARTICIPANTS 100
#define NUM_WINNERS 10
void draw_winners(int *participants, int num_participants, int num_winners) {
    for (int i = 0; i < num_winners; i++) {
        int winner_index = rand() % (num_participants - i);
        printf("当選者: %d\n", participants[winner_index]);
        participants[winner_index] = participants[num_participants - i - 1];
    }
}
int main() {
    int participants[NUM_PARTICIPANTS];
    for (int i = 0; i < NUM_PARTICIPANTS; i++) {
        participants[i] = i + 1; // 参加者番号を1から100まで設定
    }
    srand(time(NULL)); // シード値を現在の時間で設定
    draw_winners(participants, NUM_PARTICIPANTS, NUM_WINNERS);
    return 0;
}

このコードでは、draw_winners関数を使って当選者を選びます。

選ばれた当選者は配列の末尾と入れ替えられ、次の抽選から除外されます。

データシャッフル

データ分析や機械学習の分野では、データセットをシャッフルすることがよくあります。

重複しない乱数を使ってデータをシャッフルすることで、バイアスのないデータセットを作成できます。

以下に、データシャッフルの例を示します。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define DATA_SIZE 10
void shuffle_data(int *data, int size) {
    for (int i = size - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }
}
int main() {
    int data[DATA_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    srand(time(NULL)); // シード値を現在の時間で設定
    shuffle_data(data, DATA_SIZE);
    printf("シャッフルされたデータ:\n");
    for (int i = 0; i < DATA_SIZE; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");
    return 0;
}

このコードでは、shuffle_data関数を使ってデータをシャッフルしています。

rand()関数を使ってランダムなインデックスを生成し、データの位置を入れ替えています。

これらの応用例を通じて、重複しない乱数の生成方法がどのように実際のプログラムで使われるかを理解できるでしょう。

まとめ

この記事では、C言語で重複しない乱数を生成する方法について詳しく解説しました。

C言語で重複しない乱数を生成する方法は複数ありますが、それぞれの方法には特有の利点と欠点があります。

用途やパフォーマンス要件に応じて最適な方法を選択することが重要です。

目次から探す