初心者向けC言語マージソートの実装方法と詳細解説

C言語でのソートアルゴリズムの一つであるマージソートは、その安定性と効率性から広く利用されています。本記事では、マージソートの基本的な概念から具体的なC言語での実装方法まで、初心者にも分かりやすくステップバイステップで解説します。

目次

マージソートとは

マージソートは、分割統治法に基づくソートアルゴリズムの一つです。このアルゴリズムは、リストを再帰的に半分に分割し、それぞれの部分をソートしてから統合することで、全体をソートします。安定性が高く、平均計算量と最悪計算量の両方でO(n log n)の性能を持つため、非常に効率的です。

マージソートのアルゴリズム

マージソートのアルゴリズムは以下のステップで構成されています:

1. 分割

リストを半分に分割します。これを再帰的に行い、最終的にリストのサイズが1になるまで繰り返します。

2. 統合

分割したリストをソートしながら統合します。二つのソート済みリストを一つのソート済みリストにマージする手順を繰り返します。

3. 再帰

分割と統合の過程を再帰的に実行します。分割が完了した後、統合過程が逆方向に進行し、最終的に完全にソートされたリストが得られます。

この手順により、リスト全体が効率的にソートされます。次に、具体的なC言語での実装方法を見ていきましょう。

C言語での基本的なマージソート実装

ここでは、基本的なマージソートの実装方法をC言語で紹介します。以下に、全体のコード例を示します。

1. 必要なライブラリをインクルードする

まず、必要なライブラリをインクルードします。

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

2. マージ関数の実装

次に、二つの部分配列をマージする関数を実装します。

void merge(int arr[], int left, int mid, int right) {
    int i, j, k;
    int n1 = mid - left + 1;
    int n2 = right - mid;

    // 一時配列を作成
    int L[n1], R[n2];

    // データを一時配列にコピー
    for (i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];

    // 一時配列から元の配列にマージ
    i = 0;
    j = 0;
    k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    // 残りの要素をコピー
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

3. マージソート関数の実装

マージソートの再帰的な関数を実装します。

void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;

        // 左半分をソート
        mergeSort(arr, left, mid);
        // 右半分をソート
        mergeSort(arr, mid + 1, right);
        // ソートされた半分をマージ
        merge(arr, left, mid, right);
    }
}

4. メイン関数の実装

最後に、ソートを実行するメイン関数を実装します。

int main() {
    int arr[] = {12, 11, 13, 5, 6, 7};
    int arr_size = sizeof(arr)/sizeof(arr[0]);

    printf("Given array is \n");
    for (int i = 0; i < arr_size; i++)
        printf("%d ", arr[i]);
    printf("\n");

    mergeSort(arr, 0, arr_size - 1);

    printf("\nSorted array is \n");
    for (int i = 0; i < arr_size; i++)
        printf("%d ", arr[i]);
    printf("\n");
    return 0;
}

このコード例を使って、基本的なマージソートの実装方法が理解できるでしょう。

マージ関数の実装方法

マージソートの中核となるマージ関数について詳しく説明します。この関数は、二つのソート済み部分配列を一つのソート済み配列に統合する役割を担います。

1. マージ関数のプロトタイプ

まず、関数のプロトタイプを確認しましょう。

void merge(int arr[], int left, int mid, int right);

2. マージ関数の詳細な実装

以下に、マージ関数の詳細なコードを示します。この関数は、配列arrの部分配列をleftからmid、およびmid + 1からrightの範囲でマージします。

void merge(int arr[], int left, int mid, int right) {
    int i, j, k;
    int n1 = mid - left + 1;
    int n2 = right - mid;

    // 一時配列を作成
    int L[n1], R[n2];

    // データを一時配列にコピー
    for (i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];

    // 一時配列から元の配列にマージ
    i = 0;
    j = 0;
    k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    // 残りの要素をコピー
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

3. 関数の動作説明

この関数の動作は以下の通りです:

  1. 入力配列arrの二つの部分配列LRを一時的に作成します。
  2. 各部分配列をarrからコピーします。
  3. マージステップでは、LRの要素を比較し、小さい方をarrに戻します。
  4. 一方の部分配列の要素がなくなった後、残りの要素をすべてarrにコピーします。

このプロセスにより、元の配列の部分配列がソートされ、統合されます。

マージソートの再帰的な実装

マージソートは再帰アルゴリズムの代表例であり、リストを再帰的に分割してマージすることでソートを行います。ここでは、再帰的なマージソートの実装方法を詳しく解説します。

1. 再帰関数の基本構造

再帰的なマージソート関数は、リストが1つの要素になるまで分割し、その後マージしてソートされたリストを生成します。

void mergeSort(int arr[], int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;

        // 左半分をソート
        mergeSort(arr, left, mid);
        // 右半分をソート
        mergeSort(arr, mid + 1, right);
        // ソートされた半分をマージ
        merge(arr, left, mid, right);
    }
}

2. 再帰の分割ステップ

再帰ステップでは、配列を中央で二分し、それぞれの部分配列に対してmergeSort関数を再帰的に呼び出します。

if (left < right) {
    int mid = left + (right - left) / 2;

    // 左半分をソート
    mergeSort(arr, left, mid);
    // 右半分をソート
    mergeSort(arr, mid + 1, right);
}

この条件により、leftrightより小さい場合にのみ再帰が進行し、最終的にリストが1要素にまで分割されます。

3. 再帰の統合ステップ

分割が完了したら、merge関数を使ってソートされた部分配列を統合します。

merge(arr, left, mid, right);

このプロセスは、最も小さな部分配列から始まり、再帰の各ステップでより大きな部分配列が統合されていきます。

4. 全体の流れ

再帰的なマージソートの全体的な流れは次の通りです:

  1. 配列が1つの要素になるまで分割する。
  2. 分割された部分配列を再帰的にソートする。
  3. ソートされた部分配列をマージして、完全にソートされた配列を得る。

この再帰的アプローチにより、マージソートは効率的かつ安定したソートアルゴリズムとして機能します。

マージソートの非再帰的な実装

マージソートは通常、再帰を用いて実装されますが、非再帰的に実装することも可能です。非再帰的なマージソートは、イテレーティブ(反復的)なアプローチを使用し、再帰呼び出しをループで置き換えます。

1. 非再帰的マージソートの概要

非再帰的なマージソートは、リストを段階的にソートしていくアプローチを取ります。リスト全体を小さな部分に分割し、それらを順次マージしていくことでソートを実現します。

2. 非再帰的マージソートのコード例

以下に、非再帰的なマージソートのC言語での実装例を示します。

void merge(int arr[], int left, int mid, int right);

void iterativeMergeSort(int arr[], int n) {
    int curr_size;  // 現在のサイズ
    int left_start; // 左側の開始インデックス

    // 現在のサイズを1から開始し、サイズを2倍していく
    for (curr_size = 1; curr_size <= n-1; curr_size = 2*curr_size) {
        // 配列の左側部分の開始インデックスを設定
        for (left_start = 0; left_start < n-1; left_start += 2*curr_size) {
            // 中間点と右側の終了点を設定
            int mid = min(left_start + curr_size - 1, n-1);
            int right_end = min(left_start + 2*curr_size - 1, n-1);

            // 部分配列をマージ
            merge(arr, left_start, mid, right_end);
        }
    }
}

int min(int x, int y) {
    return (x < y) ? x : y;
}

void merge(int arr[], int left, int mid, int right) {
    int i, j, k;
    int n1 = mid - left + 1;
    int n2 = right - mid;

    int L[n1], R[n2];

    for (i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];

    i = 0;
    j = 0;
    k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

3. 非再帰的マージソートの動作説明

このコードは、リストを段階的にソートするために二重のループを使用します。外側のループでは現在のサイズcurr_sizeを制御し、内側のループでは部分配列の開始位置left_startを制御します。各ステップで、merge関数を使用して部分配列をマージします。

4. メイン関数での実行

以下のコードを使用して、非再帰的マージソートを実行します。

int main() {
    int arr[] = {12, 11, 13, 5, 6, 7};
    int arr_size = sizeof(arr) / sizeof(arr[0]);

    printf("Given array is \n");
    for (int i = 0; i < arr_size; i++)
        printf("%d ", arr[i]);
    printf("\n");

    iterativeMergeSort(arr, arr_size);

    printf("\nSorted array is \n");
    for (int i = 0; i < arr_size; i++)
        printf("%d ", arr[i]);
    printf("\n");
    return 0;
}

この非再帰的マージソートは、再帰を使わずに配列をソートする方法を提供します。

マージソートの時間計算量と空間計算量

マージソートの計算量について詳しく説明します。アルゴリズムの効率性を理解するためには、時間計算量と空間計算量を知ることが重要です。

1. 時間計算量

マージソートの時間計算量は、その分割統治法に基づく特性から以下のように分析されます。

最悪計算量

最悪計算量は O(n log n) です。これは、配列を再帰的に半分に分割する操作が log n 回、各分割された配列の統合に n 回の比較が必要となるためです。

平均計算量

平均計算量も O(n log n) です。全ての分割と統合操作が同じ計算量で行われるため、平均的なケースでも O(n log n) の時間がかかります。

最良計算量

最良計算量も O(n log n) です。分割と統合のプロセスは入力に依存せず一定の手順で行われるため、最良の場合でも計算量は変わりません。

2. 空間計算量

マージソートは分割と統合の過程で追加のメモリ空間を使用します。そのため、空間計算量も考慮する必要があります。

追加のメモリ使用量

マージソートでは、各再帰呼び出しで新しい部分配列を作成するため、O(n) の追加メモリが必要です。これは、各分割段階で元の配列のコピーを保持するためです。

再帰呼び出しのスタック空間

再帰的な実装では、再帰呼び出しごとにスタックフレームが作成されます。最大で log n レベルの再帰呼び出しが行われるため、スタック空間の使用量は O(log n) となります。

3. マージソートの利点と欠点

以下に、マージソートの利点と欠点をまとめます。

利点

  • 安定ソートであるため、同じ値の要素の順序が保たれる
  • 最悪計算量が O(n log n) で一定している
  • 外部ソートに適しており、大規模なデータセットでも使用可能

欠点

  • 追加のメモリ空間が必要
  • 小規模なデータセットに対しては、他のソートアルゴリズム(例:クイックソート)に比べて遅い場合がある

マージソートの時間計算量と空間計算量を理解することで、その適用範囲や利点・欠点を把握しやすくなります。

マージソートの応用例

マージソートは、その安定性と効率性から、様々な分野で応用されています。ここでは、マージソートの実際の応用例をいくつか紹介します。

1. 大規模データのソート

マージソートは、大規模データセットのソートに適しています。例えば、データベースシステムや大規模なログファイルのソートで使用されます。外部メモリソートとも呼ばれ、ハードディスク上のデータを効率的にソートすることができます。

2. データベースのマージ操作

データベース管理システムでは、複数のソート済みリストをマージして一つのソート済みリストに統合する操作が頻繁に行われます。これは、例えばクエリの結果を効率的に結合する際に利用されます。

3. 分散システムでのソート

分散システムでは、大規模なデータを複数のマシンに分散して処理する必要があります。マージソートは、分散環境でのソートにも適しており、MapReduceなどの分散処理フレームワークで使用されます。

4. 動的データのソート

リアルタイムでデータが追加されるシステムでは、データの動的なソートが必要です。マージソートは安定ソートであり、リアルタイムのデータストリームのソートにも利用されます。

5. マルチスレッド環境でのソート

マルチスレッド環境では、並列処理を利用してソートの速度を向上させることができます。マージソートは分割統治法に基づくため、自然に並列処理に適しています。各部分配列のソートを別々のスレッドで処理し、最終的にマージすることで効率的な並列ソートが実現できます。

6. コンピュータビジョンの画像処理

コンピュータビジョンの分野では、画像のピクセルデータのソートが必要になることがあります。マージソートは、ピクセルの色や輝度を基にしたソートに利用され、画像処理の前処理として効果的です。

これらの応用例からも分かるように、マージソートは非常に多用途であり、様々なシステムや環境で効率的に利用されています。

マージソートの演習問題

マージソートの理解を深めるために、以下の演習問題を解いてみましょう。これらの問題は、基本的な実装から応用まで、さまざまなレベルの問題を含んでいます。

演習問題 1: 基本的なマージソートの実装

以下の配列をマージソートを使ってソートしてください。

int arr[] = {38, 27, 43, 3, 9, 82, 10};
int n = sizeof(arr)/sizeof(arr[0]);

実装する関数:

void mergeSort(int arr[], int left, int right);
void merge(int arr[], int left, int mid, int right);

演習問題 2: 配列のサイズを入力から取得するマージソート

ユーザーから配列のサイズと要素を入力として受け取り、それをマージソートでソートするプログラムを作成してください。

ヒント:

  • scanf 関数を使用して入力を受け取ります。
  • 動的メモリ割り当てを使用して配列を作成します。

演習問題 3: 非再帰的マージソートの実装

非再帰的なマージソートを実装して、以下の配列をソートしてください。

int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr)/sizeof(arr[0]);

演習問題 4: マルチスレッドを使ったマージソートの実装

マルチスレッドを利用して、並列で動作するマージソートを実装してください。pthread ライブラリを使用すると便利です。

ヒント:

  • 各スレッドで部分配列をソートし、メインスレッドで結果をマージします。

演習問題 5: マージソートの計算量の実証

異なるサイズの配列に対してマージソートを実行し、実行時間を測定して計算量 O(n log n) を実証してください。

ヒント:

  • clock() 関数を使用して実行時間を測定します。
  • 配列サイズを徐々に増やして、実行時間の変化を観察します。

演習問題 6: 安定性の確認

マージソートの安定性を確認するために、以下の構造体をソートしてください。同じ value を持つ要素の順序がソート後も保持されていることを確認します。

struct Item {
    int value;
    char label;
};

配列例:

struct Item arr[] = {{5, 'a'}, {3, 'b'}, {5, 'c'}, {2, 'd'}, {3, 'e'}};

これらの演習問題を通じて、マージソートの理解が深まることを期待しています。

まとめ

本記事では、C言語でのマージソートの実装方法について詳細に解説しました。マージソートは、効率的で安定したソートアルゴリズムであり、分割統治法を用いることで、様々な状況で優れたパフォーマンスを発揮します。具体的なコード例や応用例、演習問題を通じて、実際にマージソートを実装し、その理解を深めることができたでしょう。これからも様々なアルゴリズムに挑戦し、プログラミングのスキルを向上させてください。

コメント

コメントする

目次