Counting Sort
最終更新
計数ソートは、既知で限られた範囲の整数に対する非比較ソートです。各値が何回出現するかを数え、その計数を使って各値を直接ソート済みの位置に書き込みます。比較は不要です。上の再生を押すと、値が集計され、そして順番に配置される様子を見られます。
計数ソートは O(n + k) の時間で動作します。ここで k は入力値の範囲です。k が n よりそれほど大きくないとき、非常に高速で O(n log n) の比較ソートを上回ることがありますが、値の範囲が巨大な場合は O(k) の計数配列によって非実用的になります。
時間計算量と空間計算量
| ケース | 計算量 | 備考 |
|---|---|---|
| 時間 | O(n + k) | n 個の要素、k = 値の範囲 |
| 空間 | O(n + k) | 計数配列 + 出力配列 |
| 安定 | はい | 接頭辞和を用いて右から左へ配置する場合 |
| 比較? | いいえ | 比較ではなく計数でソートする |
| 最適な用途 | 小さな整数の範囲 | k が n に近い場合 |
ステップごとの流れ
| ステップ | 何が起こるか |
|---|---|
| 1 | 計数配列のサイズを決めるために最大値を求める。 |
| 2 | 各値が何回出現するかを数える。 |
| 3 | (任意)安定性のために計数を接頭辞和に変換する。 |
| 4 | 各値を、出現する回数だけ出力に書き込む。 |
| 5 | 出力配列は完全にソートされている。 |
具体例
[1, 4, 1, 2, 4] をソートする(値は 0 から 4 の範囲なので計数配列は 5 つのスロットを持つ):
| パス | 状態 | 動作 |
|---|---|---|
| 入力を走査 | count = [0, 2, 1, 0, 2] | 出現回数を集計: 1 は 2 回、2 は 1 回、4 は 2 回出現する。 |
| 接頭辞和 | count = [0, 2, 3, 3, 5] | 各スロットは、そのインデックス <= の値がいくつあるかを保持し、最終的な位置を示す。 |
4 を配置 | output = [_, _, _, _, 4] | 右から左へ読む: count[4] = 5 なので 4 はインデックス 4 へ。4 にデクリメントする。 |
2 を配置 | output = [_, _, 2, _, 4] | count[2] = 3 なので 2 はインデックス 2 へ。2 にデクリメントする。 |
1 を配置 | output = [_, 1, 2, _, 4] | count[1] = 2 なので 1 はインデックス 1 へ。1 にデクリメントする。 |
4 を配置 | output = [_, 1, 2, 4, 4] | count[4] = 4 なので、この 4 はインデックス 3 へ。3 にデクリメントする。 |
1 を配置 | output = [1, 1, 2, 4, 4] | count[1] = 1 なので、この 1 はインデックス 0 へ。配列はソートされた。 |
計数ソートを使うべきとき
| 使うべきとき | 避けるべきとき |
|---|---|
| 整数(または整数に写像できるキー)を、小さく既知の範囲でソートするとき。 | 値の範囲 k が要素数 n よりはるかに大きいとき。 |
線形の O(n + k) 時間が必要で、追加の配列を許容できるとき。 | メモリが逼迫しているとき - 計数配列は n によらず O(k) を要する。 |
| サブルーチンとして安定ソートが必要なとき(例: 基数ソートの内部)。 | キーが浮動小数点数・文字列・整数へ写像できない任意のオブジェクトのとき。 |
| 最大値が有界で、事前に安価に計算できるとき。 | 範囲が未知または非有界で、計数配列のサイズを決められないとき。 |
Counting Sortのコード
Python, JavaScript, Java, C++, Cによるクリーンで実行可能なCounting Sortの実装です。言語を選んでコードをコピーするか、Coddyプレイグラウンドに読み込んだ状態で開けます。
PythonでのCounting Sortのコード
Python
1def counting_sort(a):2 # Works for non-negative integers with a small max value3 counts = [0] * (max(a) + 1)4 for value in a:5 counts[value] += 16 # Prefix sums turn counts into final positions7 for i in range(1, len(counts)):8 counts[i] += counts[i - 1]9 out = [0] * len(a)10 for value in reversed(a): # reversed keeps equal values stable11 counts[value] -= 112 out[counts[value]] = value13 return out14
15
16nums = [4, 2, 2, 8, 3, 3, 1]17print("Before:", nums)18print("After: ", counting_sort(nums))JavaScriptでのCounting Sortのコード
JavaScript
1function countingSort(arr) {2 // Count occurrences of each value, then rebuild in order3 const max = Math.max(...arr);4 const count = new Array(max + 1).fill(0);5 for (const x of arr) count[x]++;6 const out = [];7 count.forEach((c, value) => {8 for (let k = 0; k < c; k++) out.push(value);9 });10 return out;11}12
13const data = [4, 2, 9, 2, 7, 4, 1, 4];14console.log("Before:", data);15console.log("Sorted:", countingSort(data));JavaでのCounting Sortのコード
Java
1import java.util.Arrays;2
3public class Main {4 static int[] countingSort(int[] arr) {5 int max = 0;6 for (int v : arr) max = Math.max(max, v);7 int[] count = new int[max + 1];8 for (int v : arr) count[v]++;9 // Prefix sums turn counts into final positions10 for (int i = 1; i <= max; i++) count[i] += count[i - 1];11 int[] out = new int[arr.length];12 // Walk backwards so equal values keep their order (stable)13 for (int i = arr.length - 1; i >= 0; i--) {14 out[--count[arr[i]]] = arr[i];15 }16 return out;17 }18
19 public static void main(String[] args) {20 int[] arr = {4, 2, 2, 8, 3, 3, 1};21 System.out.println("Before: " + Arrays.toString(arr));22 int[] sorted = countingSort(arr);23 System.out.println("After: " + Arrays.toString(sorted));24 }25}C++でのCounting Sortのコード
C++
1#include <algorithm>2#include <iostream>3#include <vector>4
5void printVec(const std::vector<int>& a) {6 for (int x : a) std::cout << x << " ";7 std::cout << "\n";8}9
10void countingSort(std::vector<int>& a) {11 if (a.empty()) return;12 int maxVal = *std::max_element(a.begin(), a.end());13 // count[v] = how many times v appears14 std::vector<int> count(maxVal + 1, 0);15 for (int x : a) ++count[x];16 // Rebuild the array from the counts17 size_t idx = 0;18 for (int v = 0; v <= maxVal; ++v) {19 while (count[v]-- > 0) a[idx++] = v;20 }21}22
23int main() {24 std::vector<int> data = {4, 2, 2, 8, 3, 3, 1, 7};25 std::cout << "Before: ";26 printVec(data);27 countingSort(data);28 std::cout << "After: ";29 printVec(data);30 return 0;31}CでのCounting Sortのコード
C
1#include <stdio.h>2#include <stdlib.h>3
4void printArr(const int a[], int n) {5 for (int i = 0; i < n; i++) printf("%d ", a[i]);6 printf("\n");7}8
9void countingSort(int a[], int n) {10 int maxVal = a[0];11 for (int i = 1; i < n; i++) {12 if (a[i] > maxVal) maxVal = a[i];13 }14 // count[v] = how many times v appears15 int* count = calloc(maxVal + 1, sizeof(int));16 for (int i = 0; i < n; i++) count[a[i]]++;17 // Rebuild the array from the counts18 int idx = 0;19 for (int v = 0; v <= maxVal; v++) {20 while (count[v]-- > 0) a[idx++] = v;21 }22 free(count);23}24
25int main(void) {26 int data[] = {4, 2, 2, 8, 3, 3, 1, 7};27 int n = sizeof(data) / sizeof(data[0]);28 printf("Before: ");29 printArr(data, n);30 countingSort(data, n);31 printf("After: ");32 printArr(data, n);33 return 0;34}計数ソートのよくある質問
計数ソートの時間計算量は?
計数ソートは
O(n + k) です。ここで n は要素数、k は取りうる値の範囲です。k = O(n) のとき、これは線形時間になります。追加で O(n + k) の空間を使用します。計数ソートは安定ですか?
安定にできます。安定版は計数の接頭辞和を作り、要素を右から左へ配置するため、等しいキーの相対順序が保たれます。ここで示した単純な「値で書き直す」版は正しいソートを生成しますが、主に単純な整数向けに使われます。
計数ソートはいつ使うべきですか?
整数(または整数に写像できるキー)を、小さく既知の範囲でソートするときに使います。値の範囲
k が要素数よりはるかに大きい場合、計数配列はメモリを無駄にするため、比較ソートの方が適しています。計数ソートと基数ソート(radix sort)の違いは?
計数ソートは 1 回のパスで値全体を対象にソートし、値の範囲と同じ大きさの計数配列を必要とします。基数ソートは桁ごとにソートし、通常は各桁に対して安定な計数ソートを呼び出すため、1 パスあたりの範囲を小さく保てます(例: 10 進数の桁なら
10)。基数ソートは、単一の計数ソートでは非実用的になる大きな値の範囲を扱えます。なぜ計数ソートは常にクイックソート(quicksort)より速いわけではないのですか?
計数ソートは
O(n + k) なので、値の範囲 k が n と同程度のときにのみ有利です。k が巨大な場合 - 例えば 0 から 1,000,000,000 の範囲の 100 個の値をソートする場合 - O(k) の計数配列が支配的になりメモリを無駄にしますが、クイックソートのような O(n log n) の比較ソートは高速で空間効率も保たれます。計数ソートは負の数を扱えますか?
はい、小さなオフセットで扱えます。計数配列を値で直接インデックスする代わりに
value - min でインデックスし、最小値がインデックス 0 に写像されるようにします。計数配列のサイズは max - min + 1 になります。このオフセットを忘れるのはよくあるバグで、負の入力でクラッシュします。