11.10 基数ソート¶
前節では計数ソートを紹介しました。これは、データ量 \(n\) が大きく、データ範囲 \(m\) が小さい場合に適しています。\(n = 10^6\) 個の学籍番号をソートすると仮定すると、学籍番号は \(8\) 桁の数字なので、データ範囲 \(m = 10^8\) は非常に大きくなります。計数ソートでは大量のメモリ空間を確保する必要がありますが、基数ソートではこの問題を回避できます。
基数ソート(radix sort)の基本的な考え方は計数ソートと同じで、個数を数えることによってソートを実現します。そのうえで、基数ソートは各桁の段階的な関係を利用し、各桁を順にソートすることで、最終的なソート結果を得ます。
11.10.1 アルゴリズムの流れ¶
学籍番号データを例にすると、数字の最下位桁を第 \(1\) 位、最上位桁を第 \(8\) 位としたとき、基数ソートの流れは次図のようになります。
- 桁番号 \(k = 1\) を初期化します。
- 学籍番号の第 \(k\) 位に対して「計数ソート」を実行します。完了すると、データは第 \(k\) 位に従って昇順に並びます。
- \(k\) を \(1\) 増やし、手順
2.に戻って反復を続けます。すべての桁のソートが完了したら終了します。

図 11-18 基数ソートのアルゴリズムの流れ
以下ではコード実装を分解して見ていきます。\(d\) 進数の数値 \(x\) について、その第 \(k\) 位 \(x_k\) を取得するには、次の計算式を用います。
ここで、\(\lfloor a \rfloor\) は浮動小数点数 \(a\) の切り捨てを表し、\(\bmod \: d\) は \(d\) による剰余を表します。学籍番号データでは、\(d = 10\) かつ \(k \in [1, 8]\) です。
さらに、数字の第 \(k\) 位に基づいてソートできるように、計数ソートのコードを少し変更する必要があります。
def digit(num: int, exp: int) -> int:
"""要素 num の下から k 桁目を取得(exp = 10^(k-1))"""
# ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return (num // exp) % 10
def counting_sort_digit(nums: list[int], exp: int):
"""計数ソート(nums の k 桁目でソート)"""
# 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
counter = [0] * 10
n = len(nums)
# 0~9 の各数字の出現回数を集計する
for i in range(n):
d = digit(nums[i], exp) # nums[i] の第 k 位を取得し、d とする
counter[d] += 1 # 数字 d の出現回数を数える
# 累積和を求め、「出現回数」を「配列インデックス」に変換する
for i in range(1, 10):
counter[i] += counter[i - 1]
# 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
res = [0] * n
for i in range(n - 1, -1, -1):
d = digit(nums[i], exp)
j = counter[d] - 1 # d の配列内インデックス j を取得する
res[j] = nums[i] # 現在の要素をインデックス j に格納する
counter[d] -= 1 # d の個数を 1 減らす
# 結果で元の配列 nums を上書きする
for i in range(n):
nums[i] = res[i]
def radix_sort(nums: list[int]):
"""基数ソート"""
# 最大桁数の判定用に配列の最大要素を取得
m = max(nums)
# 下位桁から上位桁の順に走査する
exp = 1
while exp <= m:
# 配列要素の k 桁目に対して計数ソートを行う
# k = 1 -> exp = 1
# k = 2 -> exp = 10
# つまり exp = 10^(k-1)
counting_sort_digit(nums, exp)
exp *= 10
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
int digit(int num, int exp) {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return (num / exp) % 10;
}
/* 計数ソート(nums の k 桁目でソート) */
void countingSortDigit(vector<int> &nums, int exp) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
vector<int> counter(10, 0);
int n = nums.size();
// 0~9 の各数字の出現回数を集計する
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする
counter[d]++; // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
vector<int> res(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // d の配列内インデックス j を取得する
res[j] = nums[i]; // 現在の要素をインデックス j に格納する
counter[d]--; // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基数ソート */
void radixSort(vector<int> &nums) {
// 最大桁数の判定用に配列の最大要素を取得
int m = *max_element(nums.begin(), nums.end());
// 下位桁から上位桁の順に走査する
for (int exp = 1; exp <= m; exp *= 10)
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums, exp);
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
int digit(int num, int exp) {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return (num / exp) % 10;
}
/* 計数ソート(nums の k 桁目でソート) */
void countingSortDigit(int[] nums, int exp) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
int[] counter = new int[10];
int n = nums.length;
// 0~9 の各数字の出現回数を集計する
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする
counter[d]++; // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // d の配列内インデックス j を取得する
res[j] = nums[i]; // 現在の要素をインデックス j に格納する
counter[d]--; // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
/* 基数ソート */
void radixSort(int[] nums) {
// 最大桁数の判定用に配列の最大要素を取得
int m = Integer.MIN_VALUE;
for (int num : nums)
if (num > m)
m = num;
// 下位桁から上位桁の順に走査する
for (int exp = 1; exp <= m; exp *= 10) {
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
int Digit(int num, int exp) {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return (num / exp) % 10;
}
/* 計数ソート(nums の k 桁目でソート) */
void CountingSortDigit(int[] nums, int exp) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
int[] counter = new int[10];
int n = nums.Length;
// 0~9 の各数字の出現回数を集計する
for (int i = 0; i < n; i++) {
int d = Digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする
counter[d]++; // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int d = Digit(nums[i], exp);
int j = counter[d] - 1; // d の配列内インデックス j を取得する
res[j] = nums[i]; // 現在の要素をインデックス j に格納する
counter[d]--; // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
/* 基数ソート */
void RadixSort(int[] nums) {
// 最大桁数の判定用に配列の最大要素を取得
int m = int.MinValue;
foreach (int num in nums) {
if (num > m) m = num;
}
// 下位桁から上位桁の順に走査する
for (int exp = 1; exp <= m; exp *= 10) {
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
CountingSortDigit(nums, exp);
}
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
func digit(num, exp int) int {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return (num / exp) % 10
}
/* 計数ソート(nums の k 桁目でソート) */
func countingSortDigit(nums []int, exp int) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
counter := make([]int, 10)
n := len(nums)
// 0~9 の各数字の出現回数を集計する
for i := 0; i < n; i++ {
d := digit(nums[i], exp) // nums[i] の第 k 位を取得し、d とする
counter[d]++ // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for i := 1; i < 10; i++ {
counter[i] += counter[i-1]
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
res := make([]int, n)
for i := n - 1; i >= 0; i-- {
d := digit(nums[i], exp)
j := counter[d] - 1 // d の配列内インデックス j を取得する
res[j] = nums[i] // 現在の要素をインデックス j に格納する
counter[d]-- // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for i := 0; i < n; i++ {
nums[i] = res[i]
}
}
/* 基数ソート */
func radixSort(nums []int) {
// 最大桁数の判定用に配列の最大要素を取得
max := math.MinInt
for _, num := range nums {
if num > max {
max = num
}
}
// 下位桁から上位桁の順に走査する
for exp := 1; max >= exp; exp *= 10 {
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums, exp)
}
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
func digit(num: Int, exp: Int) -> Int {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
(num / exp) % 10
}
/* 計数ソート(nums の k 桁目でソート) */
func countingSortDigit(nums: inout [Int], exp: Int) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
var counter = Array(repeating: 0, count: 10)
// 0~9 の各数字の出現回数を集計する
for i in nums.indices {
let d = digit(num: nums[i], exp: exp) // nums[i] の第 k 位を取得し、d とする
counter[d] += 1 // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for i in 1 ..< 10 {
counter[i] += counter[i - 1]
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
var res = Array(repeating: 0, count: nums.count)
for i in nums.indices.reversed() {
let d = digit(num: nums[i], exp: exp)
let j = counter[d] - 1 // d の配列内インデックス j を取得する
res[j] = nums[i] // 現在の要素をインデックス j に格納する
counter[d] -= 1 // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for i in nums.indices {
nums[i] = res[i]
}
}
/* 基数ソート */
func radixSort(nums: inout [Int]) {
// 最大桁数の判定用に配列の最大要素を取得
var m = Int.min
for num in nums {
if num > m {
m = num
}
}
// 下位桁から上位桁の順に走査する
for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums: &nums, exp: exp)
}
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
function digit(num, exp) {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return Math.floor(num / exp) % 10;
}
/* 計数ソート(nums の k 桁目でソート) */
function countingSortDigit(nums, exp) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
const counter = new Array(10).fill(0);
const n = nums.length;
// 0~9 の各数字の出現回数を集計する
for (let i = 0; i < n; i++) {
const d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする
counter[d]++; // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for (let i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
const res = new Array(n).fill(0);
for (let i = n - 1; i >= 0; i--) {
const d = digit(nums[i], exp);
const j = counter[d] - 1; // d の配列内インデックス j を取得する
res[j] = nums[i]; // 現在の要素をインデックス j に格納する
counter[d]--; // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
/* 基数ソート */
function radixSort(nums) {
// 最大桁数の判定用に配列の最大要素を取得
let m = Math.max(... nums);
// 下位桁から上位桁の順に走査する
for (let exp = 1; exp <= m; exp *= 10) {
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
function digit(num: number, exp: number): number {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return Math.floor(num / exp) % 10;
}
/* 計数ソート(nums の k 桁目でソート) */
function countingSortDigit(nums: number[], exp: number): void {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
const counter = new Array(10).fill(0);
const n = nums.length;
// 0~9 の各数字の出現回数を集計する
for (let i = 0; i < n; i++) {
const d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする
counter[d]++; // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for (let i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
const res = new Array(n).fill(0);
for (let i = n - 1; i >= 0; i--) {
const d = digit(nums[i], exp);
const j = counter[d] - 1; // d の配列内インデックス j を取得する
res[j] = nums[i]; // 現在の要素をインデックス j に格納する
counter[d]--; // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for (let i = 0; i < n; i++) {
nums[i] = res[i];
}
}
/* 基数ソート */
function radixSort(nums: number[]): void {
// 最大桁数の判定用に配列の最大要素を取得
let m: number = Math.max(... nums);
// 下位桁から上位桁の順に走査する
for (let exp = 1; exp <= m; exp *= 10) {
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums, exp);
}
}
/* 要素 `_num` の第 k 桁を取得する。ここで `exp = 10^(k-1)` */
int digit(int _num, int exp) {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return (_num ~/ exp) % 10;
}
/* 計数ソート(nums の k 桁目でソート) */
void countingSortDigit(List<int> nums, int exp) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
List<int> counter = List<int>.filled(10, 0);
int n = nums.length;
// 0~9 の各数字の出現回数を集計する
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする
counter[d]++; // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
List<int> res = List<int>.filled(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // d の配列内インデックス j を取得する
res[j] = nums[i]; // 現在の要素をインデックス j に格納する
counter[d]--; // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for (int i = 0; i < n; i++) nums[i] = res[i];
}
/* 基数ソート */
void radixSort(List<int> nums) {
// 最大桁数の判定用に配列の最大要素を取得する
// dart の `int` の長さは 64 ビット
int m = -1 << 63;
for (int _num in nums) if (_num > m) m = _num;
// 下位桁から上位桁の順に走査する
for (int exp = 1; exp <= m; exp *= 10)
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums, exp);
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
fn digit(num: i32, exp: i32) -> usize {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return ((num / exp) % 10) as usize;
}
/* 計数ソート(nums の k 桁目でソート) */
fn counting_sort_digit(nums: &mut [i32], exp: i32) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
let mut counter = [0; 10];
let n = nums.len();
// 0~9 の各数字の出現回数を集計する
for i in 0..n {
let d = digit(nums[i], exp); // nums[i] の第 k 位を取得し、d とする
counter[d] += 1; // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for i in 1..10 {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
let mut res = vec![0; n];
for i in (0..n).rev() {
let d = digit(nums[i], exp);
let j = counter[d] - 1; // d の配列内インデックス j を取得する
res[j] = nums[i]; // 現在の要素をインデックス j に格納する
counter[d] -= 1; // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
nums.copy_from_slice(&res);
}
/* 基数ソート */
fn radix_sort(nums: &mut [i32]) {
// 最大桁数の判定用に配列の最大要素を取得
let m = *nums.into_iter().max().unwrap();
// 下位桁から上位桁の順に走査する
let mut exp = 1;
while exp <= m {
counting_sort_digit(nums, exp);
exp *= 10;
}
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
int digit(int num, int exp) {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return (num / exp) % 10;
}
/* 計数ソート(nums の k 桁目でソート) */
void countingSortDigit(int nums[], int size, int exp) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
int *counter = (int *)malloc((sizeof(int) * 10));
memset(counter, 0, sizeof(int) * 10); // 後続のメモリ解放に備えて 0 で初期化する
// 0~9 の各数字の出現回数を集計する
for (int i = 0; i < size; i++) {
// nums[i] の第 k 位を取得し、d とする
int d = digit(nums[i], exp);
// 数字 d の出現回数を数える
counter[d]++;
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
int *res = (int *)malloc(sizeof(int) * size);
for (int i = size - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // d の配列内インデックス j を取得する
res[j] = nums[i]; // 現在の要素をインデックス j に格納する
counter[d]--; // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for (int i = 0; i < size; i++) {
nums[i] = res[i];
}
// メモリを解放する
free(res);
free(counter);
}
/* 基数ソート */
void radixSort(int nums[], int size) {
// 最大桁数の判定用に配列の最大要素を取得
int max = INT32_MIN;
for (int i = 0; i < size; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
// 下位桁から上位桁の順に走査する
for (int exp = 1; max >= exp; exp *= 10)
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums, size, exp);
}
/* 要素 num の下から k 桁目を取得(exp = 10^(k-1)) */
fun digit(num: Int, exp: Int): Int {
// ここで高コストな累乗計算を繰り返さないよう、k ではなく exp を渡す
return (num / exp) % 10
}
/* 計数ソート(nums の k 桁目でソート) */
fun countingSortDigit(nums: IntArray, exp: Int) {
// 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
val counter = IntArray(10)
val n = nums.size
// 0~9 の各数字の出現回数を集計する
for (i in 0..<n) {
val d = digit(nums[i], exp) // nums[i] の第 k 位を取得し、d とする
counter[d]++ // 数字 d の出現回数を数える
}
// 累積和を求め、「出現回数」を「配列インデックス」に変換する
for (i in 1..9) {
counter[i] += counter[i - 1]
}
// 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
val res = IntArray(n)
for (i in n - 1 downTo 0) {
val d = digit(nums[i], exp)
val j = counter[d] - 1 // d の配列内インデックス j を取得する
res[j] = nums[i] // 現在の要素をインデックス j に格納する
counter[d]-- // d の個数を 1 減らす
}
// 結果で元の配列 nums を上書きする
for (i in 0..<n)
nums[i] = res[i]
}
/* 基数ソート */
fun radixSort(nums: IntArray) {
// 最大桁数の判定用に配列の最大要素を取得
var m = Int.MIN_VALUE
for (num in nums) if (num > m) m = num
var exp = 1
// 下位桁から上位桁の順に走査する
while (exp <= m) {
// 配列要素の k 桁目に対して計数ソートを行う
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// つまり exp = 10^(k-1)
countingSortDigit(nums, exp)
exp *= 10
}
}
### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###
def digit(num, exp)
# k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる
(num / exp) % 10
end
### num の第 k 桁を取得する。ここで exp = 10^(k-1) ###
def digit(num, exp)
# k ではなく exp を渡すことで、ここで高コストな累乗計算を繰り返し実行するのを避けられる
(num / exp) % 10
end
# ## 計数ソート(nums の k 桁目でソート)###
def counting_sort_digit(nums, exp)
# 10 進数の各桁は 0~9 の範囲なので、長さ 10 のバケット配列が必要
counter = Array.new(10, 0)
n = nums.length
# 0~9 の各数字の出現回数を集計する
for i in 0...n
d = digit(nums[i], exp) # nums[i] の第 k 位を取得し、d とする
counter[d] += 1 # 数字 d の出現回数を数える
end
# 累積和を求め、「出現回数」を「配列インデックス」に変換する
(1...10).each { |i| counter[i] += counter[i - 1] }
# 逆順に走査し、バケット内の集計結果に従って各要素を res に格納する
res = Array.new(n, 0)
for i in (n - 1).downto(0)
d = digit(nums[i], exp)
j = counter[d] - 1 # d の配列内インデックス j を取得する
res[j] = nums[i] # 現在の要素をインデックス j に格納する
counter[d] -= 1 # d の個数を 1 減らす
end
# 結果で元の配列 nums を上書きする
(0...n).each { |i| nums[i] = res[i] }
end
### 基数ソート ###
def radix_sort(nums)
# 最大桁数の判定用に配列の最大要素を取得
m = nums.max
# 下位桁から上位桁の順に走査する
exp = 1
while exp <= m
# 配列要素の k 桁目に対して計数ソートを行う
# k = 1 -> exp = 1
# k = 2 -> exp = 10
# つまり exp = 10^(k-1)
counting_sort_digit(nums, exp)
exp *= 10
end
end
コードの可視化
なぜ最下位桁からソートするのですか?
連続するソートの各ラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。たとえば、第1ラウンドで \(a < b\) となっていても、第2ラウンドで \(a > b\) となれば、第2ラウンドの結果が優先されます。数字では高位の優先度が低位より高いため、先に低位をソートし、その後で高位をソートする必要があります。
11.10.2 アルゴリズムの特徴¶
計数ソートと比べると、基数ソートは値の範囲が大きい場合に適しています。ただし、データが固定桁数の形式で表せること、かつ桁数が大きすぎないことが前提です。たとえば、浮動小数点数は基数ソートに適していません。桁数 \(k\) が大きすぎて、時間計算量が \(O(nk) \gg O(n^2)\) になる可能性があるためです。
- 時間計算量は \(O(nk)\)、非適応ソート:データ量を \(n\)、データが \(d\) 進数、最大桁数を \(k\) とすると、ある1桁に対して計数ソートを実行する時間は \(O(n + d)\) であり、全 \(k\) 桁をソートする時間は \(O((n + d)k)\) です。通常、\(d\) と \(k\) はどちらも比較的小さいため、時間計算量は \(O(n)\) に近づきます。
- 空間計算量は \(O(n + d)\)、非原地ソート:計数ソートと同様に、基数ソートでは長さ \(n\) と \(d\) の配列
resとcounterを補助的に用います。 - 安定ソート:計数ソートが安定であれば基数ソートも安定です。計数ソートが不安定な場合、基数ソートでは正しいソート結果を保証できません。