アルゴリズム

C言語による優先待ち行列の実装解説:ヒープを利用した効率的なデータ構造

本記事ではC言語を用いた優先待ち行列の実装方法を紹介します。

ヒープを活用する具体的な手法を解説し、要素の挿入や削除といった基本操作を効率的に行うアルゴリズムの流れについて述べます。

実際のコード例を交えながら、実環境で活用できる実用的な実装方法を分かりやすく解説します。

ヒープの基本

ヒープとは

ヒープは、優先待ち行列の実装に使用されるデータ構造の一つで、配列や木構造を用いて管理されます。

ヒープは、要素の削除や挿入の操作を効率的に行うために設計され、特に完全二分木の性質を持つため、ノードが左詰めに配置されるのが特徴です。

ヒープは一般に、以下の性質を満たします。

・「最大ヒープ」では、任意の親ノードが子ノードよりも大きい値を持ちます。

・「最小ヒープ」では、任意の親ノードが子ノードよりも小さい値を持ちます。

また、配列として実装した場合、あるノードの子や親のインデックスは以下の式で求めることができます。

{Parent index}=i12,{Left child index}=2i+1,{Right child index}=2i+2

ヒープの性質と動作

ヒープは完全二分木となるため、常に平衡状態に保たれ、木の高さが logn に制限されます。

これにより、挿入処理や削除処理の各操作の計算量が平均して O(logn) となります。

ヒープの動作は、以下の2つが主要な操作です。

・挿入処理:新しい要素を末尾に追加し、親ノードと比較しながら順次入れ替える「バブルアップ」もしくは「ヒープアップ」によってヒープ性を保ちます。

・削除処理:ルートの要素を削除し、末尾の要素と入れ替えた後、子ノードと比較しながら「バブルダウン」もしくは「ヒープダウン」の操作を行います。

優先待ち行列の設計

データ構造の定義

優先待ち行列は、ヒープを用いて実装されるため、基本的なデータ構造は配列または動的配列を利用します。

例えば、以下のような構造体を使ってヒープを定義することが可能です。

  • Heap構造体は、現在の要素数、確保している容量、要素を格納する配列を持ちます。
  • 各ノードは、優先度を表す整数値などの情報を保持します。

このように定義することで、配列インデックスを利用した効率的なアクセスが可能となり、動的な要素の増減にも対応できる設計となります。

優先度の取り扱い

優先待ち行列では、各要素に対して優先度が割り当てられ、データの取り出しや挿入の際にこの優先度を基準とします。

たとえば、最小ヒープでは、数値が小さいほど高い優先度となるため、最小の値が常にルートに来るように管理されます。

優先度の比較は、単純な大小比較を用いて行われ、挿入や削除の操作の際に親ノードや子ノードとの交換によってヒープ性が維持されます。

C言語での実装手順

ヒープの初期化

メモリ管理と初期設定

ヒープの初期化では、まず構造体 Heap を定義し、動的にメモリを確保する必要があります。

初期設定として、現在の要素数を0、容量(初期サイズ)を設定しておくことで、要素の追加時に適宜再割り当てを行うことが可能になります。

メモリ管理には標準ライブラリの mallocfree を使用し、必要な場合には realloc を用いて容量を拡張する実装を採用します。

要素の挿入処理

挿入アルゴリズムの詳細解説

要素をヒープに挿入する際は、まず新規要素をヒープの末尾に追加します。

その後、追加した要素の優先度と親ノードの優先度を比較し、ヒープの性質が保たれるまで次の操作を繰り返します。

この操作は「バブルアップ」とも呼ばれ、下記のように実行されます。

  1. 挿入した位置の親ノードを計算する。{parent}={index}12
  2. 挿入要素の優先度が親ノードより高い(最小ヒープの場合は値が小さい)場合、両者を交換する。
  3. ヒープのルートに到達するまで、または適切な位置に来るまでこの手続きを繰り返す。

要素の削除処理

削除時のヒープ再構築

削除処理は、最も優先度の高い要素(通常はルート)を取り出し、ヒープの最後尾の要素と入れ替えることで行います。

その後、入れ替えた要素が新たに配置された位置から適切な位置に移動するために、以下の手順を実行します。

  1. 現在のノードとその子ノードを比較する。
  2. 優先度の高い方(最小ヒープの場合は小さい方)の子ノードと交換する。
  3. 交換後の位置で、再び子ノードと比較しながらヒープ性が保たれるまで続行する。

この一連の操作を「バブルダウン」と呼び、ヒープ全体の構造を再構築するための重要な役割を果たします。

サンプルコードと動作解説

各関数の役割説明

以下のサンプルコードでは、ヒープを利用した優先待ち行列の基本的な操作を実装しています。

各関数の役割は次の通りです。

  • heapInit: ヒープの初期化を行い、必要なメモリを確保します。
  • insertNode: 新しい要素をヒープに挿入し、バブルアップ操作によりヒープ性を保ちます。
  • deleteNode: ルートの要素を削除し、バブルダウン操作を行ってヒープを再構築します。
  • swap: 配列内の2つの要素を交換する補助関数です。
  • main: 各関数の動作を確認するためのテスト用関数です。

コード例の詳細解説

以下に、C言語によるヒープを利用した優先待ち行列のサンプルコードを示します。

コード内には、実行可能な main関数が含まれており、標準ライブラリの #include文も記述しています。

#include <stdio.h>
#include <stdlib.h>
#define INITIAL_CAPACITY 10
// ヒープ構造体の定義
typedef struct {
    int *array;      // 要素を格納する配列
    int size;        // 現在の要素数
    int capacity;    // 配列の容量
} Heap;
// ヘルパー関数: 2つの値を交換する
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
// ヒープの初期化を行う関数
void heapInit(Heap *heap, int capacity) {
    heap->array = (int*)malloc(sizeof(int) * capacity);
    heap->size = 0;
    heap->capacity = capacity;
}
// ヒープの領域を拡張する関数
void resizeHeap(Heap *heap) {
    heap->capacity *= 2;
    heap->array = (int*)realloc(heap->array, sizeof(int) * heap->capacity);
}
// 新しい要素をヒープに挿入する関数(最小ヒープを仮定)
void insertNode(Heap *heap, int value) {
    // 容量不足の場合は領域を拡張
    if (heap->size >= heap->capacity) {
        resizeHeap(heap);
    }
    // 新規要素を追加
    int index = heap->size;
    heap->array[index] = value;
    heap->size++;
    // バブルアップ操作
    while (index != 0) {
        int parent = (index - 1) / 2;
        // 親ノードの値より小さい場合に入れ替え
        if (heap->array[index] < heap->array[parent]) {
            swap(&heap->array[index], &heap->array[parent]);
            index = parent;
        } else {
            break;
        }
    }
}
// ルートの要素を削除し、ヒープを再構築する関数(最小ヒープを仮定)
int deleteNode(Heap *heap) {
    if (heap->size == 0) {
        return -1; // ヒープが空の場合のエラー値
    }
    int rootValue = heap->array[0];
    // 最後尾の要素をルートに移動
    heap->array[0] = heap->array[heap->size - 1];
    heap->size--;
    // バブルダウン操作
    int index = 0;
    while (index < heap->size) {
        int leftChild = 2 * index + 1;
        int rightChild = 2 * index + 2;
        int smallest = index;
        // 左右の子と比較し、最小の要素を探索
        if (leftChild < heap->size && heap->array[leftChild] < heap->array[smallest]) {
            smallest = leftChild;
        }
        if (rightChild < heap->size && heap->array[rightChild] < heap->array[smallest]) {
            smallest = rightChild;
        }
        if (smallest != index) {
            swap(&heap->array[index], &heap->array[smallest]);
            index = smallest;
        } else {
            break;
        }
    }
    return rootValue;
}
// メイン関数: ヒープの動作確認用
int main() {
    Heap myHeap;
    heapInit(&myHeap, INITIAL_CAPACITY);
    // ヒープに値を挿入(例: 優先度の低い順に大きな値)
    insertNode(&myHeap, 20);  // "20"の優先度は低い
    insertNode(&myHeap, 15);
    insertNode(&myHeap, 30);
    insertNode(&myHeap, 10);  // "10"の優先度は高い
    // ヒープからルート要素を削除し、削除された要素を表示
    int minValue = deleteNode(&myHeap);
    printf("削除された要素: %d\n", minValue);
    // 残りのヒープ要素を順次削除し、表示する
    while (myHeap.size > 0) {
        printf("%d ", deleteNode(&myHeap));
    }
    printf("\n");
    // ヒープの動的確保した領域を解放
    free(myHeap.array);
    return 0;
}
削除された要素: 10
15 20 30

まとめ

この記事では、ヒープの基本とその働き、優先待ち行列の設計方法、C言語でのヒープ初期化、要素の挿入・削除手順について解説しています。

具体的なサンプルコードを通して、バブルアップおよびバブルダウンによるヒープ操作の流れや、動的メモリ管理の実装方法が理解できる内容となっています。

関連記事

Back to top button
目次へ