バケットソートを使用する

バケットソート (bucket sort) は、キーの値域を等幅の区間(バケット)に分割し、各要素を対応するバケットへ仕分けたあと、バケット内を個別に整列して連結する非比較ソートである。入力が値域上でほぼ一様に分布するとき、平均時間計算量は線形に近づく。

  1. 値域の決定: 配列の最小値 min と最大値 max から、バケット数 m(多くは n√n)と区間幅を決める。
  2. 仕分け: 各要素 x について、(x - min) / (max - min) などからバケット番号を求め、そのバケットに x を追加する。
  3. バケット内整列: 各バケットを挿入ソートやマージソートなどで昇順に整える(デモでは挿入ソート)。
  4. 連結: バケット 0, 1, … の順に要素を並べ直せば全体が昇順になる。
procedure bucket_sort(A, m)
  if length(A) = 0 then return
  minVal = minimum(A)
  maxVal = maximum(A)
  buckets = empty list of m arrays
  for each x in A
    b = bucket_index(x, minVal, maxVal, m)
    append x to buckets[b]
  for each bucket B in buckets
    sort(B)
  A = concatenate(buckets)

平均時間計算量は O(n + m + Σ n_i log n_i)n_i はバケット i の要素数)であり、分布が一様で各バケットのサイズが O(1) に収まれば O(n) となる。 最悪ではすべての要素が同一バケットに入り、内部ソートが支配的になって O(n log n) やそれ以上になりうる。 追加メモリはバケット用バッファ分 O(n + m) が典型である。

カウンティングソートや基数ソートと同様、キーの分布と値域の見積もりに依存する。浮動小数点や [0, 1) に正規化した一様乱数など、区間への写像が自然なデータに向く。サンプルソートのように比較に基づく分割点を標本から求める方式とは異なり、値域を等分する点が特徴的である。

以下のデモでは 15 要素を 5 個のバケットに仕分け、各バケットを挿入ソートで仕上げる。

計算時間量および空間計算量を計測する

Size Average time Maximum time Average memory Maximum memory
256 0.000006 0.000069 1681 1688
512 0.000013 0.001068 1702 1708
1024 0.000025 0.000489 1746 1752
2048 0.000051 0.000643 1833 1840
4096 0.000131 0.000602 2009 2016
8192 0.000211 0.000723 2175 2176
16384 0.000429 0.005347 2815 2816
32768 0.000905 0.004018 4224 4224
65536 0.001882 0.015891 7040 7040
131072 0.003822 0.005309 12686 12736
262144 0.009493 0.053594 23965 24000
計測に使用したコードを表示する

set -euo pipefail

WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT

cat > "$WORKDIR/Dockerfile" <<'EOF'
FROM rust:1.95.0

WORKDIR /app

RUN mkdir -p src

RUN cat > Cargo.toml <<'CARGO'
[package]
name = "rust-benchmark"
version = "0.1.0"
edition = "2021"

[profile.release]
lto = true
codegen-units = 1
panic = "abort"
CARGO

RUN cat > src/main.rs <<'RUST'
use std::{
    env,
    process::Command,
    time::{Duration, Instant},
};
const MIN_POWER: u32 = 8;
const MAX_POWER: u32 = 18;
const RUNS: usize = 8192;
fn insertion_sort(a: &mut [usize]) {
    for i in 1..a.len() {
        let mut j = i;
        while j > 0 && a[j - 1] > a[j] {
            a.swap(j - 1, j);
            j -= 1;
        }
    }
}



fn bucket_sort(a: &mut [usize]) {
    if a.is_empty() {
        return;
    }

    let n = a.len();
    let min = *a.iter().min().unwrap();
    let max = *a.iter().max().unwrap();
    let bucket_count = n;
    let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); bucket_count];

    for &x in a.iter() {
        let idx = if max == min {
            0
        } else {
            ((x - min) as f64 / (max - min) as f64 * (bucket_count - 1) as f64).floor() as usize
        };
        buckets[idx].push(x);
    }

    for bucket in buckets.iter_mut() {
        insertion_sort(bucket);
    }

    let mut idx = 0;
    for bucket in buckets.iter() {
        for &x in bucket.iter() {
            a[idx] = x;
            idx += 1;
        }
    }
}


fn benchmark_sort(array: &mut [usize]) {

    bucket_sort(array);

}

fn shuffled(size: usize, seed: u64) -> Vec<usize> {
    let mut v: Vec<usize> = (1..=size).collect();

    let mut state = seed;

    for i in (1..size).rev() {
        state ^= state << 13;
        state ^= state >> 7;
        state ^= state << 17;

        let j = (state as usize) % (i + 1);

        v.swap(i, j);
    }

    v
}

fn memory_usage_kb() -> usize {
    let contents = std::fs::read_to_string("/proc/self/status")
        .unwrap_or_default();

    for line in contents.lines() {
        if let Some(rest) = line.strip_prefix("VmHWM:") {
            let kb = rest
                .split_whitespace()
                .next()
                .unwrap_or("0")
                .parse::<usize>()
                .unwrap_or(0);

            return kb;
        }
    }

    0
}

fn micros(d: Duration) -> u128 {
    d.as_micros()
}

fn run_once(size: usize, seed: usize) -> (u128, usize) {
    let expected: Vec<usize> = (1..=size).collect();
    let mut array = shuffled(size, seed as u64);

    let start = Instant::now();

    benchmark_sort(&mut array);

    let elapsed = start.elapsed();

    if array != expected {
        panic!(
            "sort failed with seed {} for size {}",
            seed,
            size
        );
    }

    (micros(elapsed), memory_usage_kb())
}

fn run_child(args: &[String]) {
    let size = args[2].parse::<usize>().expect("invalid size");
    let seed = args[3].parse::<usize>().expect("invalid seed");
    let (elapsed_us, mem) = run_once(size, seed);
    println!("{} {}", elapsed_us, mem);
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.get(1).is_some_and(|arg| arg == "--run-once") {
        run_child(&args);
        return;
    }

    println!(
        "| {:>10} | {:>15} | {:>15} | {:>15} | {:>15} |",
        "Size",
        "Average time",
        "Maximum time",
        "Average memory",
        "Maximum memory"
    );

    println!(
        "|{:-<11}:|{:-<16}:|{:-<16}:|{:-<16}:|{:-<16}:|",
        "",
        "",
        "",
        "",
        ""
    );

    for power in MIN_POWER..=MAX_POWER {
        let size = 1usize << power;

        let mut total_time: u128 = 0;
        let mut max_time: u128 = 0;

        let mut total_mem: usize = 0;
        let mut max_mem: usize = 0;

        for seed in 1..=RUNS {
            let output = Command::new(env::current_exe().expect("failed to find current executable"))
                .arg("--run-once")
                .arg(size.to_string())
                .arg(seed.to_string())
                .output()
                .expect("failed to run benchmark child process");

            if !output.status.success() {
                panic!(
                    "benchmark child process failed: {}",
                    String::from_utf8_lossy(&output.stderr)
                );
            }

            let stdout = String::from_utf8(output.stdout)
                .expect("child process returned non-UTF-8 output");
            let mut fields = stdout.split_whitespace();
            let elapsed_us = fields
                .next()
                .expect("missing elapsed time")
                .parse::<u128>()
                .expect("invalid elapsed time");
            let mem = fields
                .next()
                .expect("missing memory usage")
                .parse::<usize>()
                .expect("invalid memory usage");

            total_time += elapsed_us;

            if elapsed_us > max_time {
                max_time = elapsed_us;
            }

            total_mem += mem;

            if mem > max_mem {
                max_mem = mem;
            }
        }

        let avg_time = total_time / RUNS as u128;
        let avg_mem = total_mem / RUNS;

        println!(
            "| {:>10} | {:>15} | {:>15} | {:>15} | {:>15} |",
            size,
            format!("{}.{:06}", avg_time / 1_000_000, avg_time % 1_000_000),
            format!("{}.{:06}", max_time / 1_000_000, max_time % 1_000_000),
            avg_mem,
            max_mem
        );
    }
}
RUST

RUN cargo build --release

CMD ["./target/release/rust-benchmark"]
EOF

docker build -t rust-benchmark "$WORKDIR"
docker run --rm --init rust-benchmark