[C言語] 分布数え上げソートを実装する方法

分布数え上げソート(カウントソート)は、整数の範囲が限られている場合に効率的なソートアルゴリズムです。

C言語で実装する際の基本的な手順は以下の通りです。

まず、入力配列の最大値と最小値を見つけ、その範囲に基づいてカウント配列を作成します。

次に、入力配列の各要素の出現回数をカウント配列に記録し、累積和を計算します。

最後に、累積和に基づいて入力配列をソートされた出力配列に再配置します。

この記事でわかること
  • 分布数え上げソートの基本
  • アルゴリズムの具体的な流れ
  • C言語での実装手順
  • 時間・空間計算量の特性
  • 応用例と実際の使用シーン

目次から探す

分布数え上げソートとは

分布数え上げソート(Counting Sort)は、整数の範囲が限られている場合に特に効率的なソートアルゴリズムです。

このアルゴリズムは、入力データの各要素の出現回数をカウントし、その情報を基にソートされた配列を生成します。

分布数え上げソートは、時間計算量が \(O(n + k)\) であり、ここで \(n\) は入力データの数、\(k\) はデータの範囲を示します。

このため、データの範囲が小さい場合に非常に高速に動作します。

特に、整数や特定の範囲に制約のあるデータのソートに適しています。

分布数え上げソートのアルゴリズム

アルゴリズムの基本的な流れ

分布数え上げソートのアルゴリズムは、以下の基本的な流れで実行されます。

  1. 入力データの最大値を見つける。
  2. カウント配列を作成し、各要素の出現回数をカウントする。
  3. カウント配列を基に累積和を計算する。
  4. ソートされた配列を生成するために、元の配列を逆順に走査し、カウント配列を参照して位置を決定する。

この流れにより、効率的にデータをソートすることが可能です。

カウント配列の作成

カウント配列は、入力データの各要素の出現回数を記録するための配列です。

カウント配列のサイズは、入力データの最大値に基づいて決定されます。

具体的には、次の手順で作成します。

  1. 入力データの最大値を求める。
  2. 最大値に基づいてカウント配列を初期化する。

すべての要素を0に設定する。

  1. 入力データを走査し、各要素の出現回数をカウント配列に記録する。

このカウント配列により、各要素の出現頻度を把握することができます。

累積和の計算

累積和の計算は、カウント配列を基にして行います。

累積和を計算することで、各要素がソートされた配列のどの位置に配置されるかを決定します。

具体的な手順は以下の通りです。

  1. カウント配列の最初の要素はそのままにし、2番目の要素から始めて、前の要素と加算していく。
  2. この操作をカウント配列の最後の要素まで繰り返す。

この結果、カウント配列の各要素は、元の配列におけるその要素の最終的な位置を示すことになります。

ソートされた配列の生成

ソートされた配列を生成するためには、以下の手順を実行します。

  1. 元の配列を逆順に走査する。
  2. 各要素に対して、カウント配列を参照し、その位置に要素を配置する。
  3. 配置後、カウント配列の該当要素を1減少させる。

このプロセスにより、元の配列の要素がソートされた順序で新しい配列に配置されます。

分布数え上げソートは、安定なソートアルゴリズムであるため、同じ値の要素の順序が保持されます。

C言語での分布数え上げソートの実装手順

必要な変数の準備

分布数え上げソートを実装するためには、以下の変数を準備します。

スクロールできます
変数名説明
inputArray[]ソート対象の入力配列
countArray[]各要素の出現回数を記録する配列
outputArray[]ソートされた結果を格納する配列
maxValue入力配列の最大値
n入力配列の要素数

カウント配列の初期化

カウント配列を初期化するためには、以下の手順を実行します。

  1. 入力配列の最大値を求める。
  2. 最大値に基づいてカウント配列のサイズを決定する。
  3. カウント配列を0で初期化する。

以下は、カウント配列の初期化を行うサンプルコードです。

#include <stdio.h>
void initializeCountArray(int countArray[], int maxValue) {
    for (int i = 0; i <= maxValue; i++) {
        countArray[i] = 0; // カウント配列を0で初期化
    }
}

入力配列の要素をカウントする

入力配列の要素をカウントするためには、次の手順を実行します。

  1. 入力配列を走査し、各要素の出現回数をカウント配列に記録する。

以下は、要素をカウントするサンプルコードです。

void countElements(int inputArray[], int countArray[], int n) {
    for (int i = 0; i < n; i++) {
        countArray[inputArray[i]]++; // 各要素の出現回数をカウント
    }
}

累積和を計算する

累積和を計算するためには、カウント配列を更新します。

以下のサンプルコードを参照してください。

void calculateCumulativeSum(int countArray[], int maxValue) {
    for (int i = 1; i <= maxValue; i++) {
        countArray[i] += countArray[i - 1]; // 累積和を計算
    }
}

ソートされた配列を生成する

ソートされた配列を生成するためには、元の配列を逆順に走査し、カウント配列を参照して要素を配置します。

以下はそのサンプルコードです。

void generateSortedArray(int inputArray[], int countArray[], int outputArray[], int n) {
    for (int i = n - 1; i >= 0; i--) {
        outputArray[countArray[inputArray[i]] - 1] = inputArray[i]; // ソートされた配列を生成
        countArray[inputArray[i]]--; // カウントを減少
    }
}

実装のポイントと注意点

  • 安定性: 分布数え上げソートは安定なソートアルゴリズムです。

同じ値の要素の順序が保持されます。

  • メモリ使用量: カウント配列のサイズは最大値に依存するため、範囲が広いデータには不向きです。
  • データの範囲: 整数の範囲が限られている場合に最も効果的です。

範囲が広い場合は、他のソートアルゴリズムを検討する必要があります。

分布数え上げソートの時間・空間計算量

時間計算量の解析

分布数え上げソートの時間計算量は、主に以下の3つのステップに依存します。

  1. カウント配列の作成: 入力配列の要素を走査してカウントするため、これは \(O(n)\) の時間がかかります。
  2. 累積和の計算: カウント配列を走査して累積和を計算するため、これも \(O(k)\) の時間がかかります。

ここで \(k\) は入力データの最大値です。

  1. ソートされた配列の生成: 元の配列を逆順に走査してソートされた配列を生成するため、これも \(O(n)\) の時間がかかります。

したがって、分布数え上げソートの全体の時間計算量は次のようになります。

\[O(n + k)\]

ここで、\(n\) は入力データの数、\(k\) はデータの範囲(最大値)です。

空間計算量の解析

分布数え上げソートの空間計算量は、主に以下の要素から構成されます。

  1. カウント配列: カウント配列は、最大値 \(k\) に基づいて作成されるため、空間計算量は \(O(k)\) です。
  2. 出力配列: ソートされた結果を格納するための出力配列も必要で、これも \(O(n)\) の空間を使用します。

したがって、分布数え上げソートの全体の空間計算量は次のようになります。

\[O(n + k)\]

他のソートアルゴリズムとの比較

分布数え上げソートは、特定の条件下で非常に効率的ですが、他のソートアルゴリズムと比較すると、以下のような特徴があります。

スクロールできます
ソートアルゴリズム時間計算量空間計算量特徴
分布数え上げソート\(O(n + k)\)\(O(n + k)\)整数の範囲が限られている場合に最適
クイックソート\(O(n \log n)\)\(O(\log n)\)平均的に高速だが、最悪の場合は遅い
マージソート\(O(n \log n)\)\(O(n)\)安定なソートだが、追加のメモリが必要
バブルソート\(O(n^2)\)\(O(1)\)非常に遅く、実用的ではない

分布数え上げソートは、特にデータの範囲が狭い場合に非常に効率的であり、他のソートアルゴリズムと比較しても優れた性能を発揮します。

しかし、データの範囲が広い場合には、他のアルゴリズムを選択する方が良いでしょう。

分布数え上げソートの応用例

大量の整数データのソート

分布数え上げソートは、大量の整数データを効率的にソートするために非常に適しています。

特に、データの範囲が限られている場合、例えば0から1000までの整数が大量にある場合、分布数え上げソートを使用することで、他のソートアルゴリズムよりも高速にソートを行うことができます。

この特性により、データベースや統計処理などの分野で広く利用されています。

制約のある範囲のデータのソート

分布数え上げソートは、データの範囲が明確に制約されている場合に特に効果的です。

例えば、特定の年齢層(0歳から100歳)やテストの点数(0点から100点)など、範囲が狭いデータを扱う際に、分布数え上げソートを使用することで、迅速にソートを行うことができます。

このような場合、他のソートアルゴリズムに比べて、計算時間を大幅に短縮できます。

負の数を含むデータのソート

分布数え上げソートは、負の数を含むデータのソートにも応用可能です。

負の数を扱うためには、カウント配列のインデックスを調整する必要があります。

具体的には、負の数の最小値を基準にしてカウント配列を作成し、すべての要素をその基準値でオフセットすることで、負の数を正しくカウントすることができます。

この方法により、負の数を含むデータセットでも効率的にソートが可能です。

文字列のソートへの応用

分布数え上げソートは、文字列のソートにも応用できます。

特に、文字列の各文字が特定の範囲(例えば、ASCIIコードの範囲)に収まる場合、分布数え上げソートを使用することで、文字列を効率的にソートすることができます。

例えば、固定長の文字列や、特定の文字セット(英数字など)を持つデータを扱う場合に、分布数え上げソートは非常に有効です。

このアプローチは、文字列のソートを行う際に、他のアルゴリズムよりも高速に処理を行うことができます。

よくある質問

分布数え上げソートはどのような場合に適している?

分布数え上げソートは、以下のような場合に特に適しています。

  • 整数データ: ソート対象が整数であり、かつその範囲が限られている場合。
  • 大量のデータ: 大量のデータを迅速にソートする必要がある場合。
  • 特定の範囲: データの値が特定の範囲に収まる場合(例:年齢、テストの点数など)。
  • 安定性が求められる場合: 同じ値の要素の順序を保持する必要がある場合。

このような条件下では、分布数え上げソートは非常に効率的に動作します。

負の数を含む場合、どのように実装すればよい?

負の数を含むデータを分布数え上げソートで扱う場合、以下の手順を実行します。

  1. オフセットの計算: 負の数の最小値を求め、その絶対値を基準にオフセットを計算します。
  2. カウント配列の作成: オフセットを考慮してカウント配列のサイズを決定し、すべての要素を0で初期化します。
  3. 要素のカウント: 入力配列の各要素にオフセットを加えた値をカウント配列に記録します。
  4. 累積和の計算: カウント配列の累積和を計算します。
  5. ソートされた配列の生成: 元の配列を逆順に走査し、カウント配列を参照してソートされた配列を生成します。

この方法により、負の数を含むデータでも正しくソートすることができます。

分布数え上げソートは安定なソートですか?

はい、分布数え上げソートは安定なソートアルゴリズムです。

安定なソートとは、同じ値を持つ要素の元の順序が保持されることを意味します。

分布数え上げソートでは、元の配列を逆順に走査してソートされた配列を生成するため、同じ値の要素が元の順序を維持します。

この特性により、分布数え上げソートは、安定性が求められるアプリケーションにおいても適切に使用できます。

まとめ

この記事では、分布数え上げソートの基本的な概念から実装手順、時間・空間計算量、応用例までを詳しく解説しました。

分布数え上げソートは、特に整数データのソートにおいて非常に効率的であり、特定の条件下で他のソートアルゴリズムよりも優れた性能を発揮します。

今後、データのソートが必要な際には、分布数え上げソートを検討し、その特性を活かして効率的なプログラムを実装してみてください。

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

関連カテゴリーから探す

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