Algorithm Search⌘K

Oscillating sort in Python

A tape merge scheme that alternates the direction of reading runs so that tape rewinds are avoided.

File
oscillating_sort.py, 86 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
oscillating_sort.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/oscillating-sort/. The authors are listed in prov/P385WS.jsonl.

import heapq
from typing import Protocol, TypeVar


class Ordered(Protocol):
    def __lt__(self, other: "Ordered", /) -> bool: ...


T = TypeVar("T", bound=Ordered)


class _Head:
    """The head of input d at position j; with up=False the largest key comes first."""

    __slots__ = ("x", "d", "j", "up")

    def __init__(self, x: T, d: int, j: int, up: bool) -> None:
        self.x, self.d, self.j, self.up = x, d, j, up

    def __lt__(self, other: "_Head") -> bool:
        a, b = (self.x, other.x) if self.up else (other.x, self.x)
        if a < b:
            return True
        if b < a:
            return False
        return self.d < other.d


def _merge(seqs: list[list[T]], up: bool) -> list[T]:
    """Merge sequences that are sorted in the direction up (non-decreasing) or down."""
    heap = [_Head(s[0], d, 0, up) for d, s in enumerate(seqs) if s]
    heapq.heapify(heap)
    out: list[T] = []
    while heap:
        h = heap[0]
        out.append(h.x)
        if h.j + 1 < len(seqs[h.d]):
            heapq.heapreplace(heap, _Head(seqs[h.d][h.j + 1], h.d, h.j + 1, up))
        else:
            heapq.heappop(heap)
    return out


def oscillating_sort(f: list[T], m: int = 4, t: int = 5) -> int:
    """Sort f in place with an oscillating sort on t simulated tapes and a memory of m items.

    Tape 0 is the input; tapes 1 .. t-1 are work tapes that are read backward,
    so each one is a stack of runs, and no tape is ever rewound. Precondition:
    m >= 1 and t >= 4, so that the merge order p = t - 2 is at least 2.
    Returns the number of merge levels, ceil(log_p s) for s = ceil(n / m)
    initial runs (0 if s <= 1). Each item is merged once per level; the time
    in memory is O(n log n).
    """
    if m < 1 or t < 4:
        raise ValueError("need m >= 1 and t >= 4")
    p = t - 2
    s = -(-len(f) // m)  # @line L1
    tapes: list[list[list[T]]] = [[] for _ in range(t)]
    pos = 0

    def build(n: int, x: int, up: bool) -> int:
        nonlocal pos
        if n == 1:  # @line L3
            run = sorted(f[pos:pos + m], reverse=not up)  # @line L4
            pos += m
            tapes[x].append(run)
            return 0
        others = [y for y in range(1, t) if y != x]  # @line L5
        parts = [n // p + (1 if i < n % p else 0) for i in range(p)]
        used = [(k, y) for k, y in zip(parts, others) if k > 0]
        depth = 0
        for k, y in used:  # @line L6
            depth = max(depth, build(k, y, not up))
        backward = [tapes[y].pop()[::-1] for _, y in used]  # @line L7
        tapes[x].append(_merge(backward, up))  # @line L8
        return depth + 1

    if s == 0:
        return 0
    levels = build(s, 1, True)  # @line L2
    f[:] = tapes[1].pop()
    assert not any(tapes)
    return levels
test_oscillating_sort.py 51 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/oscillating-sort/. The authors are listed in prov/P385WS.jsonl.

import random

import pytest

from oscillating_sort import oscillating_sort


def ceil_log(s: int, p: int) -> int:
    k, q = 0, 1
    while q < s:
        q *= p
        k += 1
    return k


def test_example() -> None:
    f = [5, 2, 7, 1, 8, 3, 6, 4]
    assert oscillating_sort(f, 2, 4) == 2
    assert f == list(range(1, 9))


def test_edges() -> None:
    a: list[int] = []
    assert oscillating_sort(a) == 0 and a == []
    a = [1]
    assert oscillating_sort(a) == 0 and a == [1]
    a = [3, 1, 2]
    assert oscillating_sort(a, 3, 4) == 0 and a == [1, 2, 3]
    a = [5] * 9
    oscillating_sort(a, 2, 4)
    assert a == [5] * 9
    a = list(range(50, 0, -1))
    assert oscillating_sort(a, 1, 5) == 4 and a == list(range(1, 51))
    with pytest.raises(ValueError):
        oscillating_sort([1], 2, 3)


def test_random_against_sorted() -> None:
    rng = random.Random(1962)
    for _ in range(600):
        n = rng.randint(0, 120)
        m = rng.randint(1, 6)
        t = rng.randint(4, 9)
        f = [rng.randint(-40, 40) for _ in range(n)]
        want = sorted(f)
        levels = oscillating_sort(f, m, t)
        assert f == want
        assert levels == ceil_log(-(-n // m), t - 2)

What the next level needs

  • the tests did not run in this build

Read the full page about Oscillating sort