奇偶転置ソートを使用する

奇偶転置ソート (odd-even sort) は、2種類の組に分けて隣接要素同士だけを比較し、逆順なら交換する操作を繰り返す比較ソートである。

  1. 偶数組: インデックス (0,1), (2,3), (4,5), … のペアを左サイドから順に見て、右の要素の方が小さければ入れ替える。
  2. 奇数組: インデックス (1,2), (3,4), (5,6), … のペアについて同様に比較・交換する。
  3. 収束: 偶数組と奇数組をまとめて 1 ラウンドとみなし、あるラウンドで一度も交換が起きなければ全体は昇順に整っているので終了する。

隣接ペアだけを同時に処理できるため、同じ組内の比較・交換は互いに干渉せず、ラウンド内では「飛び飛びのペア」をまとめて進められる点が特徴である。

逐次実行では最悪時間計算量は O(n²)(ラウンド数は O(n)、各ラウンドで O(n) 本の隣接ペア)、追加の配列を使わず O(1) の補助記憶でよい インプレース な実装が可能である。隣接交換のみで 安定 なソートになる。クイックソートのような分割再帰型と比べれば遅いが、構造が単純でデバッグや可視化向きである。

procedure odd_even_sort(A)
  n = length(A)
  sorted = false
  while not sorted
    sorted = true
    for i from 0 to n - 2 by 2
      if A[i] > A[i + 1] then
        swap(A[i], A[i + 1])
        sorted = false
    for i from 1 to n - 2 by 2
      if A[i] > A[i + 1] then
        swap(A[i], A[i + 1])
        sorted = false

偶・奇の相に分けて常に隣同士だけを見るため、バブルソートと並べると「先頭から順に走査するか、飛び飛びのペアで進めるか」の違いがはっきりする。

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

Size Average time Maximum time Average memory Maximum memory
256 0.000039 0.000651 1662 1668
512 0.000131 0.000471 1665 1672
1024 0.000443 0.000872 1673 1680
2048 0.001551 0.003336 1690 1696
4096 0.005781 0.011730 1722 1728
8192 0.020974 0.030058 1786 1792
16384 0.082163 0.191235 1918 1924
32768 0.332577 0.644139 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 odd_even_sort(a: &mut [usize]) {
    let mut sorted = false;
    while !sorted {
        sorted = true;
        for i in (1..a.len()).step_by(2) {
            if a[i - 1] > a[i] {
                a.swap(i - 1, i);
                sorted = false;
            }
        }
        for i in (2..a.len()).step_by(2) {
            if a[i - 1] > a[i] {
                a.swap(i - 1, i);
                sorted = false;
            }
        }
    }
}

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

    odd_even_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