二分挿入ソートを使用する

二分挿入ソート (binary insertion sort) は、挿入ソートと同じく先頭側の整列済み区間へ要素を取り込む比較ソートである。違いは、挿入位置の探索を線形走査ではなく二分探索で行う点にある。

  1. 整列済み領域: 先頭要素だけを整列済みとみなし、i = 1, 2, … の要素を順に処理する。
  2. キーの取り出し: 位置 i の値をキーと見る。
  3. 二分探索: 区間 [0, i-1] の中で、キーが収まる挿入位置 posO(log i) 回の比較で求める。
  4. シフト: [pos, i-1] の要素を右へ1つずつずらし、空いた pos にキーを置く。
  5. 終了: すべての i について繰り返すと配列全体が昇順になる。
procedure binary_insertion_sort(A)
  n = length(A)
  for i from 1 to n - 1
    key = A[i]
    lo = 0
    hi = i
    while lo < hi
      mid = floor((lo + hi) / 2)
      if A[mid] > key then
        hi = mid
      else
        lo = mid + 1
    for j from i - 1 down to lo
      A[j + 1] = A[j]
    A[lo] = key

比較回数は最悪でも O(n log n) に抑えられるが、要素の移動(シフト)は最悪 O(n²) のままである。追加配列を使わなければ補助空間は O(1) で、シフトの向きを保てば安定ソートにできる。

二分探索で比較回数だけを削っても、ランダムな入力ではシフトコストが支配的になりやすい。

通常の挿入ソートと比べ、整列済み区間が長いほど比較回数の差が出やすい。一方で要素の移動量は同じ順序で増えるため、大きな配列では全体の実行時間は依然として二次的になりがちである。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000013 0.000529 1661 1668
512 0.000040 0.000601 1665 1672
1024 0.000148 0.000731 1674 1680
2048 0.000520 0.001505 1690 1696
4096 0.001956 0.004505 1722 1728
8192 0.007196 0.033570 1786 1792
16384 0.025592 0.070849 1918 1924
32768 0.103003 0.334269 2178 2184
計測に使用したコードを表示する

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 = 15;
const RUNS: usize = 8192;
fn binary_insertion_sort(a: &mut [usize]) {
    for i in 1..a.len() {
        let key = a[i];
        let mut lo = 0;
        let mut hi = i;
        while lo < hi {
            let mid = lo + (hi - lo) / 2;
            if a[mid] > key {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        for j in (lo..i).rev() {
            a[j + 1] = a[j];
        }
        a[lo] = key;
    }
}

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

    binary_insertion_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