[C言語] 優先待ち行列を実装する方法(プライオリティーキュー)

優先待ち行列(プライオリティーキュー)は、各要素に優先度を持たせ、優先度の高い順に要素を取り出すデータ構造です。

C言語で実装するには、ヒープ(通常は二分ヒープ)を用いるのが一般的です。

ヒープは完全二分木で、親ノードの優先度が子ノードよりも高い(または低い)という性質を持ちます。

配列を使ってヒープを表現し、挿入操作や削除操作を行う際に、ヒープの性質を保つために「ヒープ化」操作(上昇・下降操作)を行います。

この記事でわかること
  • 優先待ち行列の基本
  • 基本操作の実装方法
  • 実際の応用例の紹介
  • メモリ効率の改善方法
  • 優先度の管理手法の理解

目次から探す

優先待ち行列(プライオリティーキュー)とは

優先待ち行列(プライオリティーキュー)は、各要素に優先度が付与されているデータ構造です。

通常のキューとは異なり、要素の取り出しはその優先度に基づいて行われます。

最も高い優先度を持つ要素が最初に取り出されるため、特定のタスクやイベントを効率的に管理するのに適しています。

優先待ち行列の基本

優先待ち行列の基本的な操作は以下の通りです。

スクロールできます
操作説明
挿入新しい要素を優先度と共に追加する
削除最も高い優先度の要素を取り出す
最大要素取得現在の最大優先度の要素を取得する
空判定キューが空かどうかを確認する
要素数取得現在の要素数を取得する

優先度付きデータ構造の用途

優先待ち行列はさまざまな用途で利用されます。

以下はその一部です。

スクロールできます
用途説明
タスクスケジューリングプロセスやスレッドの実行順序を管理する
グラフアルゴリズムダイクストラ法やA*アルゴリズムで使用
シミュレーションイベントの発生順序を管理する

優先待ち行列と通常のキューの違い

優先待ち行列と通常のキューの主な違いは、要素の取り出し順序にあります。

通常のキューはFIFO(先入れ先出し)方式で動作しますが、優先待ち行列は優先度に基づいて要素を取り出します。

以下の表にその違いを示します。

スクロールできます
特徴通常のキュー優先待ち行列
取り出し順序先入れ先出し(FIFO)優先度に基づく
要素の優先度なしあり
使用例一般的なデータ処理タスク管理、アルゴリズム

優先待ち行列の基本操作

優先待ち行列の基本操作は、データ構造の機能を理解する上で重要です。

以下に、各操作の詳細を説明します。

要素の挿入

要素を優先度と共に優先待ち行列に追加する操作です。

新しい要素は、適切な位置に挿入され、優先度に基づいて整列されます。

以下は、要素を挿入するサンプルコードです。

#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
    int data;          // データ
    int priority;     // 優先度
    struct Node* next; // 次のノードへのポインタ
} Node;
Node* insert(Node* head, int data, int priority) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->priority = priority;
    newNode->next = NULL;
    if (head == NULL || head->priority < priority) {
        newNode->next = head;
        return newNode;
    }
    Node* current = head;
    while (current->next != NULL && current->next->priority >= priority) {
        current = current->next;
    }
    newNode->next = current->next;
    current->next = newNode;
    return head;
}
int main() {
    Node* head = NULL;
    head = insert(head, 10, 1); // 優先度1の要素を挿入
    head = insert(head, 20, 3); // 優先度3の要素を挿入
    head = insert(head, 30, 2); // 優先度2の要素を挿入
    Node* current = head;
    while (current != NULL) {
        printf("データ: %d, 優先度: %d\n", current->data, current->priority);
        current = current->next;
    }
    return 0;
}
データ: 20, 優先度: 3
データ: 30, 優先度: 2
データ: 10, 優先度: 1

要素の削除

最も高い優先度の要素を削除する操作です。

削除後は、次に高い優先度の要素が先頭に来るように整列されます。

以下は、要素を削除するサンプルコードです。

Node* delete(Node* head) {
    if (head == NULL) {
        return NULL; // キューが空の場合
    }
    Node* temp = head;
    head = head->next; // 最も高い優先度の要素を削除
    free(temp); // メモリを解放
    return head;
}

最大(または最小)要素の取得

現在の最大優先度の要素を取得する操作です。

要素を削除せずに、優先度の高い要素を確認できます。

以下は、最大要素を取得するサンプルコードです。

Node* getMax(Node* head) {
    return head; // 最も高い優先度の要素を返す
}

空の判定

優先待ち行列が空かどうかを確認する操作です。

空の場合は、NULLを返します。

以下は、空の判定を行うサンプルコードです。

int isEmpty(Node* head) {
    return head == NULL; // 空なら1、そうでなければ0を返す
}

要素数の取得

現在の要素数を取得する操作です。

キュー内の要素をカウントして返します。

以下は、要素数を取得するサンプルコードです。

int getSize(Node* head) {
    int count = 0;
    Node* current = head;
    while (current != NULL) {
        count++;
        current = current->next;
    }
    return count; // 要素数を返す
}

優先待ち行列の実装手順

優先待ち行列を実装するためには、まずデータ構造を定義し、基本的な操作を実装する必要があります。

以下にその手順を詳しく説明します。

データ構造の定義

優先待ち行列を実装するための基本的なデータ構造を定義します。

ここでは、ノードを表す構造体を作成し、データと優先度を保持します。

typedef struct Node {
    int data;          // データ
    int priority;     // 優先度
    struct Node* next; // 次のノードへのポインタ
} Node;

初期化関数の実装

優先待ち行列を初期化する関数を実装します。

この関数は、空のキューを作成します。

Node* initialize() {
    return NULL; // 空のキューを返す
}

挿入関数の実装

新しい要素を優先度と共に挿入する関数を実装します。

挿入時に優先度に基づいて適切な位置に配置します。

Node* insert(Node* head, int data, int priority) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->priority = priority;
    newNode->next = NULL;
    if (head == NULL || head->priority < priority) {
        newNode->next = head;
        return newNode;
    }
    Node* current = head;
    while (current->next != NULL && current->next->priority >= priority) {
        current = current->next;
    }
    newNode->next = current->next;
    current->next = newNode;
    return head;
}

削除関数の実装

最も高い優先度の要素を削除する関数を実装します。

この関数は、削除後に次の要素を返します。

Node* delete(Node* head) {
    if (head == NULL) {
        return NULL; // キューが空の場合
    }
    Node* temp = head;
    head = head->next; // 最も高い優先度の要素を削除
    free(temp); // メモリを解放
    return head;
}

ヒープ化関数の実装

優先待ち行列をヒープを用いて実装する場合、ヒープ化のための関数を実装します。

ここでは、最小ヒープまたは最大ヒープを構築するための基本的な手法を示します。

void heapify(Node* arr[], int n, int i) {
    int largest = i; // 親ノード
    int left = 2 * i + 1; // 左の子ノード
    int right = 2 * i + 2; // 右の子ノード
    if (left < n && arr[left]->priority > arr[largest]->priority) {
        largest = left;
    }
    if (right < n && arr[right]->priority > arr[largest]->priority) {
        largest = right;
    }
    if (largest != i) {
        Node* temp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = temp;
        heapify(arr, n, largest); // 再帰的にヒープ化
    }
}

優先度の比較方法

優先度を比較するための関数を実装します。

この関数は、2つの優先度を比較し、どちらが高いかを判断します。

int comparePriority(int priority1, int priority2) {
    return priority1 - priority2; // 優先度の差を返す
}

これらの関数を組み合わせることで、優先待ち行列を効果的に実装することができます。

完成したサンプルコード

以下に、優先待ち行列を実装した完成したサンプルコードを示します。

このコードでは、ノードの定義、挿入、削除、最大要素の取得、空の判定、要素数の取得などの基本操作を含んでいます。

#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
    int data;          // データ
    int priority;     // 優先度
    struct Node* next; // 次のノードへのポインタ
} Node;
// キューを初期化する関数
Node* initialize() {
    return NULL; // 空のキューを返す
}
// 要素を挿入する関数
Node* insert(Node* head, int data, int priority) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->priority = priority;
    newNode->next = NULL;
    if (head == NULL || head->priority < priority) {
        newNode->next = head;
        return newNode;
    }
    Node* current = head;
    while (current->next != NULL && current->next->priority >= priority) {
        current = current->next;
    }
    newNode->next = current->next;
    current->next = newNode;
    return head;
}
// 要素を削除する関数
Node* delete(Node* head) {
    if (head == NULL) {
        return NULL; // キューが空の場合
    }
    Node* temp = head;
    head = head->next; // 最も高い優先度の要素を削除
    free(temp); // メモリを解放
    return head;
}
// 最大要素を取得する関数
Node* getMax(Node* head) {
    return head; // 最も高い優先度の要素を返す
}
// 空の判定を行う関数
int isEmpty(Node* head) {
    return head == NULL; // 空なら1、そうでなければ0を返す
}
// 要素数を取得する関数
int getSize(Node* head) {
    int count = 0;
    Node* current = head;
    while (current != NULL) {
        count++;
        current = current->next;
    }
    return count; // 要素数を返す
}
// メイン関数
int main() {
    Node* head = initialize(); // キューを初期化
    // 要素を挿入
    head = insert(head, 10, 1); // 優先度1の要素を挿入
    head = insert(head, 20, 3); // 優先度3の要素を挿入
    head = insert(head, 30, 2); // 優先度2の要素を挿入
    // 現在の要素数を表示
    printf("現在の要素数: %d\n", getSize(head));
    // 最大要素を取得して表示
    Node* maxNode = getMax(head);
    if (maxNode != NULL) {
        printf("最大要素: データ: %d, 優先度: %d\n", maxNode->data, maxNode->priority);
    }
    // 要素を削除
    head = delete(head);
    printf("要素を削除しました。\n");
    // 現在の要素数を表示
    printf("現在の要素数: %d\n", getSize(head));
    // キューが空かどうかを確認
    if (isEmpty(head)) {
        printf("キューは空です。\n");
    } else {
        printf("キューには要素があります。\n");
    }
    // メモリを解放
    while (!isEmpty(head)) {
        head = delete(head);
    }
    return 0;
}

出力結果

現在の要素数: 3
最大要素: データ: 20, 優先度: 3
要素を削除しました。
現在の要素数: 2
キューには要素があります。

このサンプルコードは、優先待ち行列の基本的な機能を示しており、挿入、削除、最大要素の取得、空の判定、要素数の取得が行えます。

実行することで、優先待ち行列の動作を確認できます。

優先待ち行列の応用例

優先待ち行列は、さまざまな分野での効率的なデータ管理に利用されています。

以下に、いくつかの具体的な応用例を示します。

タスクスケジューリング

タスクスケジューリングでは、プロセスやスレッドの実行順序を管理するために優先待ち行列が使用されます。

各タスクには優先度が設定され、CPUが実行するタスクは最も高い優先度を持つものから順に選ばれます。

これにより、重要なタスクが迅速に処理され、システム全体の効率が向上します。

例えば、リアルタイムシステムでは、緊急性の高いタスクが優先的に実行される必要があります。

ダイクストラ法による最短経路探索

ダイクストラ法は、グラフ内の最短経路を見つけるためのアルゴリズムで、優先待ち行列を利用して効率的に動作します。

各ノードには、スタートノードからの距離が優先度として設定され、最も距離が短いノードが優先的に処理されます。

これにより、全てのノードを効率的に探索し、最短経路を見つけることができます。

優先待ち行列を使用することで、アルゴリズムの時間計算量が大幅に改善されます。

シミュレーションシステムでのイベント管理

シミュレーションシステムでは、イベントが発生する順序を管理するために優先待ち行列が利用されます。

各イベントには発生時刻や重要度が設定され、優先待ち行列に追加されます。

シミュレーションは、最も早く発生するイベントから順に処理され、システムの動作をリアルタイムで模擬します。

これにより、複雑なシミュレーションでも効率的にイベントを管理し、正確な結果を得ることが可能になります。

よくある質問

優先度が同じ場合はどう処理する?

優先度が同じ場合、通常は先入れ先出し(FIFO)方式で処理されます。

つまり、同じ優先度を持つ要素が複数存在する場合、最初に挿入された要素が最初に取り出されます。

この動作を実現するためには、リストの末尾に新しい要素を追加し、削除時にはリストの先頭から取り出すように実装します。

これにより、同じ優先度の要素の順序が保持されます。

ヒープ以外の実装方法はある?

はい、優先待ち行列はヒープ以外にもさまざまなデータ構造で実装できます。

例えば、以下のような方法があります。

  • 配列: 固定サイズの配列を使用して、要素を挿入する際にソートする方法。
  • リンクリスト: リンクリストを使用して、挿入時に適切な位置に要素を追加する方法。
  • バイナリサーチツリー: バイナリサーチツリーを使用して、優先度に基づいて要素を管理する方法。

それぞれの方法には利点と欠点があり、使用するシナリオに応じて選択することが重要です。

優先待ち行列のメモリ効率を改善する方法は?

優先待ち行列のメモリ効率を改善するためには、以下の方法が考えられます。

  • 動的メモリ管理: 必要なときにのみメモリを割り当て、使用しなくなったメモリを解放することで、メモリの無駄遣いを防ぎます。
  • メモリプールの利用: あらかじめ一定量のメモリを確保し、必要に応じてその中からメモリを割り当てることで、メモリの断片化を防ぎます。
  • データ構造の選択: 使用するデータ構造を見直し、より効率的なものを選択することで、メモリ使用量を削減します。

例えば、ヒープを使用することで、挿入や削除の際のメモリ操作を最小限に抑えることができます。

これらの方法を組み合わせることで、優先待ち行列のメモリ効率を向上させることが可能です。

まとめ

この記事では、C言語における優先待ち行列の基本的な概念や実装方法、応用例について詳しく解説しました。

優先待ち行列は、タスクスケジューリングや最短経路探索、イベント管理など、さまざまな分野で重要な役割を果たしています。

これを機に、優先待ち行列を実際のプログラミングやアルゴリズムの実装に活用してみてはいかがでしょうか。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す