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
# 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