コムソートを使用する

コムソート (comb sort) は、一定の間隔(ギャップ)で離れた2要素どうしを比較し、逆順なら交換する操作を繰り返す比較ソートである。

間隔をだんだん狭め、最終的に間隔 1(隣同士)の走査だけが残ると、振る舞いはバブルソートに近づく。粗い間隔から先に大まかな秩序を作るため、同じ O(n²) 系でもバブル単体より実効速度が出やすい。

  1. ギャップの設定: 配列長 n から出発し、各フェーズでギャップを縮小係数(典型値は約 1.3)で割り、1 未満になる場合は 1 に丸める。
  2. 走査と交換: 索引 i について A[i]A[i + gap] を比較し、A[i] > A[i + gap] なら交換する。i0 から n - 1 - gap まで進める。
  3. 繰り返し条件: ギャップが 1 より大きい または、直前のフェーズで交換が一度でも起きた限り、手順 1〜2 を続ける。ギャップが 1 で、交換のない走査が終われば完了。

Stephen Lacey と Richard Box が提案した経験的な縮小率 1.3 に加え、小さなギャップが 9 または 10 になることを避ける Comb sort 11を併用する実装がよく知られている。

procedure comb_sort(A)
  n = length(A)
  gap = n
  shrink = 1.3
  swapped = true
  while gap > 1 or swapped
    gap = floor(gap / shrink)
    if gap < 1 then
      gap = 1
    if gap == 9 or gap == 10 then   // Comb sort 11
      gap = 11
    swapped = false
    for i from 0 to n - 1 - gap
      if A[i] > A[i + gap] then
        swap(A[i], A[i + gap])
        swapped = true

最悪時間計算量は O(n²) だが、バブルソートに比べ「遠く離れた大きい値・小さい値」の入替えが早い。空間計算量は O(1) のインプレース、等しいキーの順序は一般に保証されない 不安定 ソートである。

最終的には間隔 1 に落ちてバブルソートと同種の「隣との比較」だけが続くので、バブルと並べて眺めると違いがはっきりする。アルゴリズムは単純でインプレースだが最悪は O(n²) のままなので実務での第一選択ではないことも頭に置いておくとよい。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000007 0.000529 1662 1668
512 0.000018 0.000471 1666 1672
1024 0.000039 0.000572 1673 1680
2048 0.000086 0.000509 1689 1696
4096 0.000189 0.000582 1721 1728
8192 0.000420 0.000779 1786 1792
16384 0.000914 0.001136 1917 1924
32768 0.002005 0.002808 2177 2184
65536 0.004378 0.006883 2690 2696
131072 0.008900 0.013253 3713 3720
262144 0.019996 0.082279 5762 5768
計測に使用したコードを表示する

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 comb_sort(a: &mut [usize]) {
    let mut gap = a.len();
    let mut swapped = true;
    while gap > 1 || swapped {
        gap = (gap * 10 / 13).max(1);
        swapped = false;
        for i in 0..a.len().saturating_sub(gap) {
            if a[i] > a[i + gap] {
                a.swap(i, i + gap);
                swapped = true;
            }
        }
    }
}

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

    comb_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