【C言語】マージソートとは?プログラムの書き方や仕組みを解説

この記事では、マージソートというソートアルゴリズムについて詳しく解説します。

マージソートの基本的な概念や仕組み、C言語での実装方法、そしてその利点や欠点について学ぶことができます。

初心者の方でもわかりやすく説明しているので、ぜひ最後まで読んでみてください。

目次から探す

マージソートの基本概念

マージソートの定義

マージソートは、効率的なソートアルゴリズムの一つで、特に大規模なデータセットを扱う際に有用です。

このアルゴリズムは、配列を再帰的に分割し、それぞれの部分をソートした後に、再び結合(マージ)することで全体をソートします。

マージソートは「安定なソート」であり、同じ値を持つ要素の順序を保持します。

分割統治法の概要

マージソートは「分割統治法」に基づいています。

この手法は、問題を小さな部分に分割し、それぞれを解決した後に結果を統合するというアプローチです。

具体的には、以下の3つのステップから成り立っています。

  1. 分割: 配列を2つの部分に分けます。
  2. 征服: 各部分を再帰的にマージソートを用いてソートします。
  3. 統合: ソートされた2つの部分をマージして、1つのソートされた配列を作成します。

この方法により、マージソートは効率的にデータを処理することができます。

マージソートの特徴

マージソートにはいくつかの特徴があります。

  • 時間計算量: マージソートの最悪および平均の時間計算量はO(n log n)です。

これは、分割とマージの過程で、各要素がlog n回の分割を経て、n回のマージを行うためです。

  • 空間計算量: マージソートは追加の配列を必要とするため、空間計算量はO(n)です。

これは、ソートするために新しい配列を作成する必要があるためです。

  • 安定性: マージソートは安定なソートアルゴリズムです。

つまり、同じ値を持つ要素の相対的な順序が保持されます。

これにより、特定の条件に基づいてデータをソートする際に便利です。

  • 適用性: マージソートは、外部ソート(ディスク上のデータをソートする場合)にも適しており、大規模なデータセットを扱う際に特に効果的です。

これらの特徴から、マージソートは多くの場面で利用されるソートアルゴリズムとなっています。

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

マージソートは、効率的なソートアルゴリズムの一つで、分割統治法を用いています。

このセクションでは、マージソートのアルゴリズムの流れや再帰的アプローチについて詳しく解説します。

アルゴリズムの流れ

マージソートのアルゴリズムは、主に以下の2つのステップから成り立っています。

配列の分割

まず、与えられた配列を2つの部分配列に分割します。

この分割は、配列の要素数が1になるまで続けられます。

配列の要素数が1の場合、その配列はすでにソートされていると見なされます。

例えば、配列 [38, 27, 43, 3, 9, 82, 10] を分割すると、次のようになります。

  1. [38, 27, 43, 3, 9, 82, 10][38, 27, 43][3, 9, 82, 10]
  2. [38, 27, 43][38][27, 43]
  3. [27, 43][27][43]

このように、配列は再帰的に分割されていきます。

ソートされた部分配列のマージ

次に、分割された部分配列をソートしながらマージします。

マージの際には、2つのソートされた部分配列を比較し、より小さい要素を新しい配列に追加していきます。

このプロセスを繰り返すことで、最終的に全体がソートされた配列が得られます。

例えば、先ほどの例で [27][43] をマージすると、 [27, 43] になります。

次に、これを [38] とマージすると、 [27, 38, 43] となります。

再帰的アプローチ

マージソートは再帰的に実行されるため、再帰の仕組みを理解することが重要です。

再帰の仕組み

再帰とは、関数が自分自身を呼び出すことを指します。

マージソートでは、配列を分割するために自分自身を呼び出し、最終的にソートされた配列を得るために再帰的にマージを行います。

以下は、再帰的にマージソートを実行する際の擬似コードです。

function mergeSort(array):
    if length(array) <= 1:
        return array
    mid = length(array) / 2
    left = mergeSort(array[0:mid])
    right = mergeSort(array[mid:length(array)])
    return merge(left, right)

このように、配列が1つの要素になるまで再帰的に分割し、その後にマージを行います。

ベースケースの設定

再帰関数には必ずベースケースが必要です。

マージソートの場合、配列の長さが1以下のときがベースケースです。

この条件を満たすと、再帰が終了し、ソートされた配列が返されます。

ベースケースを設定することで、無限ループを防ぎ、正しくソートされた結果を得ることができます。

C言語におけるマージソートの実装

マージソートをC言語で実装する際の全体構成や各関数の役割について詳しく見ていきましょう。

プログラムの全体構成

マージソートのプログラムは、主に以下の要素で構成されます。

ヘッダファイルのインクルード

まず、必要なヘッダファイルをインクルードします。

標準入出力を使用するために<stdio.h>を、メモリ操作を行うために<stdlib.h>をインクルードします。

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

メイン関数の役割

次に、プログラムのエントリーポイントであるmain関数を定義します。

この関数では、ソートしたい配列を定義し、マージソートを呼び出します。

int main() {
    int arr[] = {38, 27, 43, 3, 9, 82, 10}; // ソート対象の配列
    int arr_size = sizeof(arr) / sizeof(arr[0]); // 配列のサイズ
    mergeSort(arr, 0, arr_size - 1); // マージソートの呼び出し
    printf("ソート後の配列: \n");
    for (int i = 0; i < arr_size; i++) {
        printf("%d ", arr[i]); // ソート後の配列を表示
    }
    return 0;
}

マージ関数の実装

マージソートの中心となるのが、配列をマージするための関数です。

この関数では、2つのソートされた部分配列を結合します。

マージ処理の詳細

マージ処理では、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 = (int *)malloc(n1 * sizeof(int));
    int *R = (int *)malloc(n2 * sizeof(int));
    // 左部分配列のコピー
    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++;
    }
    // 一時配列のメモリを解放
    free(L);
    free(R);
}

配列のコピーと結合

上記のmerge関数では、まず左と右の部分配列をそれぞれ一時的な配列にコピーし、その後、要素を比較しながら元の配列に戻していきます。

この過程で、配列の要素が正しい順序で配置されることになります。

ソート関数の実装

次に、実際にマージソートを行うためのソート関数を実装します。

この関数は再帰的に呼び出され、配列を分割していきます。

再帰的な呼び出し

ソート関数は、配列を分割し、各部分をソートした後にマージ関数を呼び出します。

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);
    }
}

分割処理の実装

mergeSort関数では、配列の左端と右端を比較し、分割が可能な場合は再帰的に自分自身を呼び出します。

最終的に、すべての部分配列がソートされた後、merge関数を使ってそれらを結合します。

このようにして、C言語におけるマージソートの実装が完成します。

プログラム全体を通して、分割とマージのプロセスがどのように行われるかを理解することが重要です。

完成したコード

完成したコードがこちらです

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

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 = (int *)malloc(n1 * sizeof(int));
    int *R = (int *)malloc(n2 * sizeof(int));
    // 左部分配列のコピー
    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++;
    }
    // 一時配列のメモリを解放
    free(L);
    free(R);
}

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);
    }
}

int main() {
    int arr[] = {38, 27, 43, 3, 9, 82, 10}; // ソート対象の配列
    int arr_size = sizeof(arr) / sizeof(arr[0]); // 配列のサイズ
    mergeSort(arr, 0, arr_size - 1); // マージソートの呼び出し
    printf("ソート後の配列: \n");
    for (int i = 0; i < arr_size; i++) {
        printf("%d ", arr[i]); // ソート後の配列を表示
    }
    return 0;
}

この用にプログラムを組み立てることで、マージソートを行うことができます。

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

マージソートは、効率的なソートアルゴリズムの一つであり、特に大規模なデータセットに対して優れた性能を発揮します。

このセクションでは、マージソートの時間計算量と空間計算量について詳しく解説します。

時間計算量

マージソートの時間計算量は、データのサイズに対してどのように変化するかを示します。

一般的に、マージソートはO(n log n)の時間計算量を持ちます。

ここで、nはソートする要素の数です。

最悪の場合の計算量

最悪の場合の計算量は、データが逆順に並んでいる場合など、最も非効率的な状況を考えたときの計算量です。

マージソートは、常に分割してマージするため、最悪の場合でもO(n log n)の時間計算量を維持します。

これは、分割の過程でlog n回の分割が行われ、各分割でn回の比較が行われるためです。

平均の場合の計算量

平均の場合の計算量も、最悪の場合と同様にO(n log n)です。

データがランダムに並んでいる場合でも、マージソートは効率的に動作します。

したがって、マージソートは安定したパフォーマンスを提供するため、特に大規模なデータセットに適しています。

空間計算量

空間計算量は、アルゴリズムが実行される際に必要とするメモリの量を示します。

マージソートは、追加の配列を使用してデータをマージするため、空間計算量はO(n)となります。

追加メモリの使用

マージソートでは、ソートする配列と同じサイズの一時的な配列を使用します。

この配列は、分割された部分配列をマージする際に必要です。

したがって、n個の要素を持つ配列をソートする場合、nのサイズの追加メモリが必要になります。

効率的なメモリ管理

マージソートの空間計算量は、他のソートアルゴリズムと比較して高いですが、効率的なメモリ管理を行うことで、メモリの使用を最小限に抑えることができます。

例えば、インプレースマージソートのような手法を用いることで、追加のメモリ使用を減らすことが可能です。

ただし、インプレースマージソートは実装が複雑になるため、一般的には標準的なマージソートが使用されます。

このように、マージソートは時間計算量がO(n log n)で安定しており、空間計算量はO(n)であるため、大規模データのソートにおいて非常に有用なアルゴリズムです。

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

マージソートは、効率的なソートアルゴリズムとして広く利用されていますが、利点と欠点があります。

ここでは、マージソートの特性を詳しく見ていきましょう。

利点

安定性

マージソートの大きな利点の一つは、その「安定性」です。

安定なソートアルゴリズムとは、同じ値を持つ要素の順序がソート後も保持されることを意味します。

例えば、元の配列において AB という同じ値を持つ要素があり、AがBの前にあった場合、マージソートを適用した後もAがBの前に来ることが保証されます。

この特性は、特に複数のキーでソートを行う場合に重要です。

大規模データへの適用性

マージソートは、大規模なデータセットに対して非常に効果的です。

特に、データが外部メモリ(ディスクなど)に存在する場合、マージソートはその特性を活かして効率的にソートを行うことができます。

外部ソートアルゴリズムとしてのマージソートは、データを小さなチャンクに分割し、それぞれをメモリ内でソートした後、マージすることで全体をソートします。

この方法は、メモリの制約がある場合でも大規模データを扱うことができるため、実用的です。

欠点

空間効率の悪さ

マージソートの欠点の一つは、空間効率が悪いことです。

マージ処理を行うために、追加の配列を必要とします。

このため、元の配列のサイズに比例したメモリを消費します。

特に、大きなデータセットを扱う場合、メモリの使用量が問題になることがあります。

これに対して、クイックソートなどの他のソートアルゴリズムは、より少ないメモリで動作することができます。

小規模データへの不向き

マージソートは、一般的に大規模データに対して優れた性能を発揮しますが、小規模データに対しては必ずしも最適ではありません。

小さな配列に対しては、オーバーヘッドが大きくなり、他のアルゴリズム(例えば、挿入ソートや選択ソート)の方が効率的に動作することがあります。

したがって、データのサイズに応じて適切なソートアルゴリズムを選択することが重要です。

以上のように、マージソートには多くの利点がありますが、特定の状況では欠点も存在します。

これらを理解することで、適切な場面でマージソートを活用することができるでしょう。

マージソートの応用例

大規模データのソート

マージソートは、大規模なデータセットを効率的にソートするために広く使用されています。

特に、データがメモリに収まりきらない場合、マージソートの特性が活かされます。

データを小さな部分に分割し、それぞれをソートした後にマージすることで、全体を効率的に整列させることができます。

この手法は、特に外部メモリ(ディスクなど)を使用する場合に有効です。

外部ソートアルゴリズム

外部ソートは、メモリに収まりきらない大規模データをソートするための手法です。

マージソートはこの外部ソートの代表的なアルゴリズムの一つです。

データを複数のブロックに分割し、それぞれをメモリ内でソートした後、ソートされたブロックをマージしていくことで、全体を整列させます。

この方法は、ディスクI/Oの回数を最小限に抑えることができるため、効率的です。

データベースのソート処理

データベースにおいても、マージソートは重要な役割を果たします。

データベースは通常、大量のデータを扱うため、効率的なソートアルゴリズムが必要です。

マージソートは、データベースのクエリ処理やインデックス作成時に使用され、特に大規模なデータセットに対して安定したパフォーマンスを提供します。

データベースのソート処理では、マージソートの安定性が特に重要であり、同じ値を持つデータの順序を保持することが求められます。

マージソートの重要性

マージソートは、その効率性と安定性から、さまざまな分野で重要な役割を果たしています。

特に、データの整列が必要な場面では、マージソートの特性が活かされます。

大規模データの処理や外部ソート、データベースのソート処理など、実際のアプリケーションにおいても多くの場面で利用されています。

これにより、データの検索や分析が迅速に行えるようになり、ビジネスや研究の現場での意思決定をサポートしています。

マージソートは、今後もデータ処理の基盤として重要な役割を果たし続けるでしょう。

目次から探す