双方向挿入ソートを使用する

双方向挿入ソート (two-way insertion sort) は、挿入ソートと同じく整列済み区間へ要素を取り込む比較ソートである。先頭要素から [0, last] を整列済み領域とみなし、新しい値が最小側・最大側・中間のどれに当たるかで挿入方向を分ける点が特徴である。

  1. 整列済み領域: 先頭要素だけを整列済みとみなし、last = 0 から始める。
  2. 左端への挿入: 位置 i の値が A[0] より小さいとき、区間 [0, last] を右へ1つずつずらし、空いた先頭へ置く。
  3. 右端への挿入: 値が A[last] 以上のとき、last を1つ伸ばし、右端側へシフトしてから A[last] に置く。
  4. 中間への挿入: それ以外は [0, last] 内を線形探索し、見つけた位置の右側をシフトして挿入する。
  5. 終了: すべての i について繰り返すと [0, last] が配列全体となり昇順になる。
procedure two_way_insertion_sort(A)
  n = length(A)
  if n <= 1 then
    return
  last = 0
  for i from 1 to n - 1
    if A[i] < A[0] then
      key = A[i]
      for j from last down to 0
        A[j + 1] = A[j]
      A[0] = key
      last = last + 1
    else if A[i] >= A[last] then
      last = last + 1
      key = A[i]
      for j from i - 1 down to last
        A[j + 1] = A[j]
      A[last] = key
    else
      k = last
      while A[k] > A[i]
        k = k - 1
      key = A[i]
      for j from i - 1 down to k + 1
        A[j + 1] = A[j]
      A[k + 1] = key
      last = last + 1

比較回数・要素の移動はいずれも最悪 O(n²) で、補助空間は O(1) である。整列済みに近い入力では右端への追記が多くなり、逆順に近い入力では左端への挿入が続く。いずれも通常の挿入ソートより走査方向を選べるが、中間挿入では線形探索が残る。

二分挿入ソートが比較回数を削るのに対し、双方向挿入ソートは整列済み区間の両端への追記を早めに分岐させる。どちらも大域的な二次時間は避けられないが、入力の偏りによっては通常の挿入ソートよりステップが少なくなる場合がある。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000012 0.000649 1662 1668
512 0.000048 0.000613 1666 1672
1024 0.000184 0.002053 1673 1680
2048 0.000713 0.005327 1690 1696
4096 0.002678 0.009174 1721 1728
8192 0.010626 0.024431 1786 1792
16384 0.041445 0.142906 1918 1924
32768 0.170173 0.729362 2177 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 two_way_insertion_sort(a: &mut [usize]) {
    if a.is_empty() {
        return;
    }

    let mut last = 0usize;

    for i in 1..a.len() {
        if a[i] < a[0] {
            let key = a[i];
            for j in (0..=last).rev() {
                a[j + 1] = a[j];
            }
            a[0] = key;
            last += 1;
        } else if a[i] >= a[last] {
            last += 1;
            let key = a[i];
            for j in (last..i).rev() {
                a[j + 1] = a[j];
            }
            a[last] = key;
        } else {
            let mut k = last;
            while a[k] > a[i] {
                k -= 1;
            }
            let key = a[i];
            for j in (k + 1..i).rev() {
                a[j + 1] = a[j];
            }
            a[k + 1] = key;
            last += 1;
        }
    }
}


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

    two_way_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