RedEnginePress logo
RedEnginePress
AlgorithmsLanguagesPlaygroundAbout

Merge Sort Tiger

M
//! Tiger Style Merge Sort - Zero Recursion Implementation
//!
//! Demonstrates Tiger Style principles:
//! - Iterative bottom-up merge sort (no recursion)
//! - Explicit u32 indices (never usize)
//! - Heavy assertions on all array accesses
//! - Bounded loops with provable upper bounds
//! - Fail-fast on invalid inputs
//! - Simple, explicit control flow

const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;

/// Maximum array size we support (must be bounded)
pub const MAX_ARRAY_SIZE: u32 = 1_000_000;

/// Tiger Style merge sort - sorts array A using work buffer B
/// Both arrays must have identical length <= MAX_ARRAY_SIZE
/// Time: O(n log n), Space: O(n) for work buffer
pub fn sort(comptime T: type, A: []T, B: []T) void {
    // Preconditions - assert all inputs
    assert(A.len == B.len);
    assert(A.len <= MAX_ARRAY_SIZE);

    const n: u32 = @intCast(A.len);

    // Handle trivial cases
    if (n <= 1) {
        // Postcondition: trivial arrays are already sorted
        return;
    }

    // Copy A to B for initial pass
    copyArray(T, A, 0, n, B);

    // Bottom-up iterative merge sort
    // No recursion! Loop bound: log2(n) iterations
    var width: u32 = 1;
    var iteration: u32 = 0;
    const max_iterations: u32 = 32; // log2(MAX_ARRAY_SIZE) = ~20, use 32 for safety

    while (width < n) : (iteration += 1) {
        // Assert bounded loop
        assert(iteration < max_iterations);
        assert(width > 0);
        assert(width <= n);

        // Merge subarrays of size 'width'
        var i: u32 = 0;
        const merge_count_max = n / width + 1; // Upper bound on merges this iteration
        var merge_count: u32 = 0;

        while (i < n) : (merge_count += 1) {
            // Assert bounded inner loop
            assert(merge_count <= merge_count_max);
            assert(i < n);

            const left = i;
            const middle = @min(i + width, n);
            const right = @min(i + 2 * width, n);

            // Invariants
            assert(left < middle);
            assert(middle <= right);
            assert(right <= n);

            // Merge on alternating passes
            if (iteration % 2 == 0) {
                merge(T, B, left, middle, right, A);
            } else {
                merge(T, A, left, middle, right, B);
            }

            i = right;
        }

        width = width * 2;

        // Postcondition: width increased
        assert(width > 0); // Check for overflow
    }

    // If even number of iterations, result is in B, copy back to A
    if (iteration % 2 == 0) {
        copyArray(T, B, 0, n, A);
    }

    // Postcondition: array is sorted (verified in tests)
}

/// Merge two sorted subarrays from A into B
/// Merges A[begin..middle) with A[middle..end) into B[begin..end)
fn merge(
    comptime T: type,
    A: []const T,
    begin: u32,
    middle: u32,
    end: u32,
    B: []T,
) void {
    // Preconditions
    assert(begin <= middle);
    assert(middle <= end);
    assert(end <= A.len);
    assert(end <= B.len);
    assert(A.len <= MAX_ARRAY_SIZE);
    assert(B.len <= MAX_ARRAY_SIZE);

    var i: u32 = begin; // Index for left subarray
    var j: u32 = middle; // Index for right subarray
    var k: u32 = begin; // Index for output

    // Merge with explicit bounds
    const iterations_max = end - begin;
    var iterations: u32 = 0;

    while (k < end) : ({
        k += 1;
        iterations += 1;
    }) {
        // Assert bounded loop
        assert(iterations <= iterations_max);
        assert(k < end);
        assert(k < B.len);

        // Choose from left or right subarray
        if (i < middle and (j >= end or A[i] <= A[j])) {
            // Take from left
            assert(i < A.len);
            B[k] = A[i];
            i += 1;
        } else {
            // Take from right
            assert(j < A.len);
            assert(j < end);
            B[k] = A[j];
            j += 1;
        }

        // Invariants
        assert(i <= middle);
        assert(j <= end);
    }

    // Postconditions
    assert(k == end);
    assert(i == middle or j == end); // One subarray exhausted
}

/// Copy elements from A to B in range [begin, end)
fn copyArray(
    comptime T: type,
    A: []const T,
    begin: u32,
    end: u32,
    B: []T,
) void {
    // Preconditions
    assert(begin <= end);
    assert(end <= A.len);
    assert(end <= B.len);
    assert(A.len <= MAX_ARRAY_SIZE);
    assert(B.len <= MAX_ARRAY_SIZE);

    var k: u32 = begin;
    const iterations_max = end - begin;
    var iterations: u32 = 0;

    while (k < end) : ({
        k += 1;
        iterations += 1;
    }) {
        // Assert bounded loop
        assert(iterations <= iterations_max);
        assert(k < A.len);
        assert(k < B.len);

        B[k] = A[k];
    }

    // Postcondition
    assert(k == end);
}

/// Verify array is sorted in ascending order
fn isSorted(comptime T: type, array: []const T) bool {
    assert(array.len <= MAX_ARRAY_SIZE);

    if (array.len <= 1) return true;

    const n: u32 = @intCast(array.len);
    var i: u32 = 1;

    while (i < n) : (i += 1) {
        assert(i < array.len);
        if (array[i - 1] > array[i]) {
            return false;
        }
    }

    return true;
}

// ============================================================================
// Tests - Exhaustive edge case coverage
// ============================================================================

test "sort: empty array" {
    const array: []i32 = &.{};
    const work: []i32 = &.{};

    sort(i32, array, work);

    try testing.expect(isSorted(i32, array));
}

test "sort: single element" {
    var array: [1]i32 = .{42};
    var work: [1]i32 = .{0};

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    try testing.expectEqual(@as(i32, 42), array[0]);
}

test "sort: two elements sorted" {
    var array: [2]i32 = .{ 1, 2 };
    var work: [2]i32 = @splat(0);

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    try testing.expectEqual(@as(i32, 1), array[0]);
    try testing.expectEqual(@as(i32, 2), array[1]);
}

test "sort: two elements reversed" {
    var array: [2]i32 = .{ 2, 1 };
    var work: [2]i32 = @splat(0);

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    try testing.expectEqual(@as(i32, 1), array[0]);
    try testing.expectEqual(@as(i32, 2), array[1]);
}

test "sort: already sorted" {
    var array: [10]i32 = .{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    var work: [10]i32 = @splat(0);

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    for (array, 0..) |value, i| {
        try testing.expectEqual(@as(i32, @intCast(i + 1)), value);
    }
}

test "sort: reverse order" {
    var array: [10]i32 = .{ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
    var work: [10]i32 = @splat(0);

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    for (array, 0..) |value, i| {
        try testing.expectEqual(@as(i32, @intCast(i + 1)), value);
    }
}

test "sort: duplicates" {
    var array: [8]i32 = .{ 5, 2, 8, 2, 9, 1, 5, 5 };
    var work: [8]i32 = @splat(0);

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    // Verify specific values
    try testing.expectEqual(@as(i32, 1), array[0]);
    try testing.expectEqual(@as(i32, 2), array[1]);
    try testing.expectEqual(@as(i32, 2), array[2]);
}

test "sort: all same elements" {
    var array: [10]i32 = @splat(7);
    var work: [10]i32 = @splat(0);

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    for (array) |value| {
        try testing.expectEqual(@as(i32, 7), value);
    }
}

test "sort: negative numbers" {
    var array: [6]i32 = .{ -5, -1, -10, 0, -3, -7 };
    var work: [6]i32 = @splat(0);

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    try testing.expectEqual(@as(i32, -10), array[0]);
    try testing.expectEqual(@as(i32, 0), array[5]);
}

test "sort: large array power of 2" {
    var array: [256]i32 = undefined;
    var work: [256]i32 = undefined;

    // Initialize with reverse order
    for (&array, 0..) |*elem, i| {
        elem.* = @intCast(255 - i);
    }

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
    for (array, 0..) |value, i| {
        try testing.expectEqual(@as(i32, @intCast(i)), value);
    }
}

test "sort: large array non-power of 2" {
    var array: [1000]i32 = undefined;
    var work: [1000]i32 = undefined;

    // Initialize with pseudorandom pattern
    for (&array, 0..) |*elem, i| {
        elem.* = @intCast((i * 7919) % 1000);
    }

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
}

test "sort: stress test - verify no recursion stack overflow" {
    // This would overflow stack with recursive implementation
    var array: [10000]i32 = undefined;
    var work: [10000]i32 = undefined;

    // Worst case: reverse sorted
    for (&array, 0..) |*elem, i| {
        elem.* = @intCast(9999 - i);
    }

    sort(i32, &array, &work);

    try testing.expect(isSorted(i32, &array));
}

test "sort: different types - u32" {
    var array: [5]u32 = .{ 5, 2, 8, 1, 9 };
    var work: [5]u32 = @splat(0);

    sort(u32, &array, &work);

    try testing.expect(isSorted(u32, &array));
}

test "sort: different types - u64" {
    var array: [5]u64 = .{ 5, 2, 8, 1, 9 };
    var work: [5]u64 = @splat(0);

    sort(u64, &array, &work);

    try testing.expect(isSorted(u64, &array));
}