Table of Contents
ソートタスクが、グレード、年齢、または分類コードなどの小さな整数の大きな配列を含む場合、QuickSortやMergeSortなどの古典的な比較ベースのアルゴリズムは、オーバーキルのように感じることができます。 これらのアルゴリズムは、O(n log n) で実行されますが、可能な値の範囲が制限されている場合は、[FLT比較:0]] で線形O(n + k) 時刻をソートできます。 ソートは、両方の要素を正確に計算するだけでなく、両方の要素を識別することができます。
ソートのカウント方法
ソートは、入力値が小さな範囲から描画される整数であるという知識を悪用します ]。 対等比較の代わりに、値の周波数のヒストグラムを構築し、そのヒストグラムが正しいソートされた位置で各要素を配置するために使用されます。
基本的アプローチ:直接再構築
カウントの最もシンプルなバージョンのソートは、2つのパスで動作します。
- [周波数] - 入力配列を繰り返し、各値のカウンターを増やします。
- []inputを上書きします。 - 最小から最大まで、各値のカウンター配列を移動し、その数として何度も入力配列に書き戻します。
これはソートされた出力を収めますが、]not[]は、重複の相対的な順序(それは安定しません)を保持します。 同じキーでレコードの元の順序を維持しながら、キーをソートするときに安定性の問題。 続いて説明された安定した変形は、最も一般的に練習で使用されます。
安定的な変化:累積計算
ソートを安定させるには、次の3番目のパスを追加します。
- 頻度を前後にカウントします。
- 周波数配列を累積数配列に変換します。このステップの後、は要素の個数を占める ≤ ]]]i] を保持します。
- 逆に入力配列を反復します(最後の要素から最初の要素へ)。各要素では、その累積数を使用して、出力配列のその位置を見つけ、それを置き、カウントを損なう。
逆に横向きなので、等しい要素の相対的な順序が保存されます。出力配列は入力とは別ですから、このバージョンでは出力用のO(n)追加スペースを使用します。つまり、基本バージョンは入力を上書きすることで、内部に並べ替えることができます。
C#でカウントのソートを実行
以下は、C# の実装です。基本的なインプレースバージョン(安定性が不要なシナリオの場合)と、補助配列を使用する安定したバージョンです。どちらも、事前に最大値を知る必要があります。
基本(非安定)カウントのソート
このバリアントは、出力バッファなしで直接入力配列をソートします。メモリの効率性は高く、安定しません。
public static void CountingSortBasic(int[] array, int maxValue)
{
int[] counts = new int[maxValue + 1];
// Count each element's frequency
for (int i = 0; i < array.Length; i++)
{
counts[array[i]]++;
}
// Overwrite the original array in sorted order
int index = 0;
for (int value = 0; value <= maxValue; value++)
{
while (counts[value]-- > 0)
{
array[index++] = value;
}
}
}
安定したカウントのソート
安定したバージョンでは、入力と同じサイズの出力配列が必要です。また、累積カウントを使用して、要素を正しく位置します。
public static int[] CountingSortStable(int[] array, int maxValue)
{
int[] counts = new int[maxValue + 1];
int[] output = new int[array.Length];
// Step 1: Count occurrences
foreach (int num in array)
{
counts[num]++;
}
// Step 2: Transform counts to cumulative counts
for (int i = 1; i <= maxValue; i++)
{
counts[i] += counts[i - 1];
}
// Step 3: Build the output array (iterate input in reverse for stability)
for (int i = array.Length - 1; i >= 0; i--)
{
int value = array[i];
output[counts[value] - 1] = value;
counts[value]--;
}
return output;
}
両方の実装では、[は配列に表示されている最大の整数です。真の最大が不明な場合は、あらかじめ用意されているスキャン(O(n))で計算できます。 安定したバージョンは、新しいソートされた配列を返します。元の変更は解除されます。
複雑化解析
[n] は、要素数と[]k = max - min + 1 (可能な値の範囲) です。
- [Time:]] カウントソートが]O(n + k)時間で実行されます。 カウントフェーズはO(n)で、累積プレフィックスはO(k)であり、再構成はO(n)です。 kがO(n)の場合、アルゴリズムは線形です。
- [ スペース:]] 基本バージョンは、カウント配列のO(k) 余分スペースを使用します。 安定したバージョンは、出力配列を割り当てるので、O(n + k) を使用します。 これは、範囲が項目の数に大きく相対的にあるときに、ソートをカウントすることができません。
- []:[] 比較 - QuickSortやMergeSortなどの比較 - 比較は、少なくともO(nログn)の比較が必要です。 小さなk(例えば、k<10,000とn>100,000)の場合、ソートをカウントすると、拡大度が速く注文することができます。
バリエーションとエクステンション
負の整数を扱う
ソートをネイティブにカウントすると、非負の整数で動作します。負の値を処理するには、範囲全体をシフトして最小限にゼロになります。例えば、数字が-1000から1000の範囲であれば、すべての要素を+1000でオフセットします。カウント配列は]のサイズを持っています。
public static int[] CountingSortWithNegative(int[] array)
{
if (array.Length == 0) return array;
int min = array.Min();
int max = array.Max();
int range = max - min + 1;
int[] counts = new int[range];
int[] output = new int[array.Length];
foreach (int num in array)
counts[num - min]++;
for (int i = 1; i < range; i++)
counts[i] += counts[i - 1];
for (int i = array.Length - 1; i >= 0; i--)
{
int value = array[i];
output[counts[value - min] - 1] = value;
counts[value - min]--;
}
return output;
}
非整数キーマッピング
ソートのカウントには整数キーが必要です。データが文字(バイト)、または整数にキャストできる列挙で構成されている場合、あなたはまだそれを適用することができます。より大きなオブジェクトの場合、整数キーを抽出し、オブジェクトを並べ替えることができます。これは、Ridexソートが内部のサブルーチンとしてソートをカウントすることが多い方法です。
Radix ソート コロンボ
Radix ソートは、それぞれに数字(またはビット)を処理します。そして、ソートをカウントすると、ベース(例えば、10または256)が小さいときに、各パスの自然な選択です。これにより、任意の整数の線形時間ソートが、小さなものではなく、できます。
C#の実践的検討
記憶フットプリントおよび大きいk
最大の落とし穴は、利用可能なメモリよりも大きい数の配列を割り当てています。例えば、1,000要素を1,000,000の廃棄物スペースの範囲でソートします。常に[kが、nよりも大きい大きさの注文ではないことを確認します。
パラレルリズムとスパン<T>
非常に大きな配列では、入力をスレッド間で分割することで、カウントフェーズを並列化できます。各スレッドはセグメントをプライベート配列にカウントし、部分的な結果が集計されます。とを使用して、カウント配列は範囲が小さいときにヒープ割り当てを減らすことができます。
エッジケース
- ]空の配列[] - すぐに返します。
- []単体要素 - ソートはトリバイアルです。
- []] - カウント配列は1つの非ゼロエントリを持ちます。 O(n)で再構成が実行されます。
- [] は、大幅な範囲が、データを間隔で間隔をあて – カウントソートは、ほとんどのカウントエントリがゼロであるため、非効率になります。ハッシュベースのカウントアプローチまたはバケットソートを検討してください。
パフォーマンスの提言
入力整数が小さい範囲に落ちるを知っているとき、カウント ソートを使用してください。 (例えば、0–100 を等級別にし、0–120 を年齢回し、またはエラー コード 0–255)。 より大きい範囲では、Radix ソートまたは、高範囲のパーティションの QuickSort にフォールドするハイブリッドを検討してください。
カウントソートを使用するときに(およびないとき)
| Situation | Recommendation |
|---|---|
| Small integer range (k ~ n) | Excellent choice – linear time, simple code. |
| Large integer range (k >> n) | Avoid – memory waste and O(k) overhead. |
| Need stability | Use the stable variant (cumulative counts). |
| Strings or objects | Consider Radix Sort or a comparison sort. |
| Extremely large datasets | Counting Sort can be parallelized; but watch memory. |
ベンチマークとパフォーマンス
n = 1,000,000とk = 1,000の典型的なベンチマークでは、ソートはによって撮影された時間の約20〜30%で完了します。 (これは、イントロソートを使用する)。 kが減少するギャップは、幅が広がります。 以下は、近似比較(.NET 8で現代のCPUの実行時間)です。
n = 1,000,000 | k = 1,000
Array.Sort (QuickSort variant) : 68 ms
CountingSortBasic : 12 ms
CountingSortStable : 18 ms
範囲が10,000に成長すると、ソートがまだ勝ちますが、マージンが狭くなります。 k = 100,000の場合、メモリオーバーヘッド(- ≈ 400 KB カウントアレイ)はCPUキャッシュを傷つけ、パフォーマンスが劣化します。
コンテンツ
ソートをカウントすることは、データがその制約に合うときに線形性能を提供する非受容的に単純なアルゴリズムです。 C# 開発者にとっては、小さな整数の配列を扱うため、ソート時間を劇的に減らすことができる貴重なツールです。 データの範囲に目がかりましょう。 小さくて知られる場合は、カウント‐ は、比較ベースの代替手段を上回ります。 より一般的なソートには、組み込みの を使用しますが、常に、数文字をカウントする準備が整数と並び替え時に、数をカウントする準備が整数をカウントする準備が整います。
更に読むには、]の項目をカウントするWikipediaの記事の]]のMicrosoft docsのArray.Sort[]のMicrosoft docs、およびの実用的なガイドを参照してください。