Algorithm Search⌘K

Library sort in Python

Insertion sort into an array with deliberate gaps, giving expected O(n log n) time with few moves.

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

import random
from typing import Optional, Protocol, TypeVar


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


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


Slot = Optional[tuple[T, int]]


def _before(x: T, i: int, y: T, j: int) -> bool:
    """Order by key, then by input position, so that all keys are distinct."""
    if x < y:
        return True
    return not y < x and i < j


def _spread(items: list[tuple[T, int]], size: int) -> list[Slot]:
    """Place the items in order over size slots, with even gaps."""
    s: list[Slot] = [None] * size
    k = len(items)
    for j, x in enumerate(items):
        s[(2 * j + 1) * size // (2 * k)] = x
    return s


def _insert(s: list[Slot], x: T, i: int) -> None:
    size = len(s)
    lo, hi = 0, size  # @line L9
    while lo < hi:
        mid = (lo + hi) // 2
        j = mid
        while j < hi and s[j] is None:
            j += 1
        if j == hi:
            hi = mid
        else:
            y = s[j]
            assert y is not None
            if _before(x, i, y[0], y[1]):
                hi = mid
            else:
                lo = j + 1
    q = lo - 1  # @line L10
    while q >= 0 and s[q] is None:
        q -= 1
    r = lo
    while r < size and s[r] is None:
        r += 1
    if r - q > 1:  # @line L11
        s[(q + r) // 2] = (x, i)
        return
    e = r + 1  # @line L12
    while e < size and s[e] is not None:
        e += 1
    if e < size:
        s[r + 1 : e + 1] = s[r:e]
        s[r] = (x, i)
        return
    e = q - 1  # @line L13
    while s[e] is not None:
        e -= 1
    s[e:q] = s[e + 1 : q + 1]
    s[q] = (x, i)


def library_sort(a: list[T], eps: float = 1.0, seed: Optional[int] = 2004) -> None:
    """Sort a in non-decreasing order by insertion into a gapped array.

    The items are inserted in a random order. Before each round, which doubles
    the number of inserted items, the items are spread evenly over about
    (1 + eps) times as many slots as the round will need. Each insertion finds
    its place by binary search and shifts items only up to the nearest gap.
    Ties are broken by input position, so the sort is stable. With seed=None
    the input order is kept as the insertion order.
    Cost: O(n log n) time with high probability, O((1 + eps) n) extra space.
    """
    n = len(a)
    if n < 2:
        return
    order = list(range(n))
    if seed is not None:  # @line L1
        random.Random(seed).shuffle(order)
    k = 1  # @line L2
    items: list[tuple[T, int]] = [(a[order[0]], order[0])]
    while k < n:  # @line L3
        m = min(2 * k, n)  # @line L4
        size = int((1 + eps) * m) + 1
        s = _spread(items, size)  # @line L5
        for t in range(k, m):  # @line L6
            p = order[t]
            _insert(s, a[p], p)
        k = m  # @line L7
        items = [x for x in s if x is not None]
    a[:] = [x for x, _ in items]  # @line L8
test_library_sort.py 79 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/library-sort/. The authors are listed in prov/QMZWKB.jsonl.

import itertools
import random

import library_sort as module
from library_sort import library_sort


class Key:
    def __init__(self, k: int, tag: int) -> None:
        self.k, self.tag = k, tag

    def __lt__(self, other: "Key") -> bool:
        return self.k < other.k


def test_example() -> None:
    a = [5, 1, 4, 2, 8, 0, 3, 6]
    library_sort(a, 1.0, None)
    assert a == [0, 1, 2, 3, 4, 5, 6, 8]


def test_example_slots() -> None:
    s = module._spread([(1, 0), (2, 1), (4, 2), (5, 3)], 17)
    assert [i for i, x in enumerate(s) if x is not None] == [2, 6, 10, 14]
    for p, x in ((4, 8), (5, 0), (6, 3), (7, 6)):
        module._insert(s, x, p)
    assert ["_" if x is None else x[0] for x in s] == [0, "_", 1, "_", "_", "_", 2, "_", 3, "_", 4, "_", "_", "_", 5, 6, 8]


def test_edges() -> None:
    for a in ([], [1], [2, 1], [1, 2], [1, 1, 1], [3, 2, 1], [2**63 - 1, -(2**63)], [2**63 - 1, 0, -(2**63), 2**63 - 1]):
        for seed in (None, 7):
            b = list(a)
            library_sort(b, 1.0, seed)
            assert b == sorted(a)


def test_all_permutations() -> None:
    for n in range(8):
        for p in itertools.permutations(range(n)):
            b = list(p)
            library_sort(b)
            assert b == list(range(n))


def test_small_eps_and_no_shuffle() -> None:
    rng = random.Random(3)
    for eps in (0.05, 0.25, 3.0):
        for seed in (None, 11):
            a = [rng.randint(0, 50) for _ in range(500)]
            b = list(a)
            library_sort(b, eps, seed)
            assert b == sorted(a)


def test_stable() -> None:
    rng = random.Random(2006)
    for top in (0, 3, 1000):
        a = [Key(rng.randint(0, top), i) for i in range(400)]
        library_sort(a)
        assert [(e.k, e.tag) for e in a] == sorted((e.k, e.tag) for e in a)


def test_random_against_sorted() -> None:
    rng = random.Random(2004)
    for _ in range(600):
        a = [rng.randint(-40, 40) for _ in range(rng.randint(0, 120))]
        b = list(a)
        library_sort(b, 1.0, rng.randrange(1000))
        assert b == sorted(a)


def test_strings() -> None:
    a = ["pear", "fig", "apple", "fig", "kiwi"]
    library_sort(a)
    assert a == ["apple", "fig", "fig", "kiwi", "pear"]

What the next level needs

  • the tests did not run in this build

Read the full page about Library sort