Algorithm Search⌘K

Han-Thorup randomized integer sorting in Python

Randomized word-RAM integer sort with expected time O(n sqrt(log log n)), faster than deterministic bounds.

File
han_thorup_integer_sort.py, 65 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
han_thorup_integer_sort.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/han-thorup-integer-sort/. The authors are listed in prov/B8DZ6B.jsonl.
import random
from bisect import bisect_left
from math import isqrt


def _insertion_sort(a: list[int]) -> list[int]:
    for i in range(1, len(a)):
        x = a[i]
        j = i - 1
        while j >= 0 and a[j] > x:
            a[j + 1] = a[j]
            j -= 1
        a[j + 1] = x
    return a


def han_thorup_integer_sort(a: list[int], base: int = 8, seed: int | None = None) -> list[int]:
    """Return the integers of a in non-decreasing order.

    Precondition: a holds integers (any sign) and base >= 2. This is a model of
    the structure of the Han and Thorup algorithm: split the keys into ordered
    parts by a random sample of about sqrt(n) splitters, check that no part is
    too large, and recurse. The paper splits with signature sort and packed
    words in O(n sqrt(log log n)) expected time in total. Here each key is
    placed by binary search, so the expected time is O(n log n), and the extra
    space is O(n). The output does not depend on the seed.
    """
    if base < 2:
        raise ValueError("base must be at least 2")
    return _sort(list(a), base, random.Random(seed))


def _split(a: list[int], splitters: list[int]) -> tuple[list[list[int]], list[int]]:
    s = len(splitters)
    parts: list[list[int]] = [[] for _ in range(s + 1)]
    equal = [0] * s
    for x in a:
        j = bisect_left(splitters, x)
        if j < s and splitters[j] == x:
            equal[j] += 1
        else:
            parts[j].append(x)
    return parts, equal


def _sort(a: list[int], base: int, rng: random.Random) -> list[int]:
    n = len(a)
    if n <= base:  # @line L1
        return _insertion_sort(a)
    k = isqrt(n - 1) + 1  # @line L2
    for attempt in range(4):
        sample = [a[rng.randrange(n)] for _ in range(k)]  # @line L3
        p = _sort(sample, base, rng)  # @line L4
        splitters = [x for i, x in enumerate(p) if i == 0 or x != p[i - 1]]
        parts, equal = _split(a, splitters)  # @line L5
        if max(len(q) for q in parts) <= n // 2 or attempt == 3:  # @line L6
            break
    out: list[int] = []
    for j in range(len(parts)):  # @line L7
        out.extend(_sort(parts[j], base, rng))
        if j < len(equal):
            out.extend([splitters[j]] * equal[j])
    return out  # @line L8
test_han_thorup_integer_sort.py 41 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/han-thorup-integer-sort/. The authors are listed in prov/B8DZ6B.jsonl.
import random
import unittest

from han_thorup_integer_sort import _split, han_thorup_integer_sort


class TestHanThorupIntegerSort(unittest.TestCase):
    def test_page_example(self):
        a = [41, 7, 93, 18, 65, 30, 7, 88, 51, 24, 76, 3, 59, 12, 97, 35]
        parts, equal = _split(a, [18, 51, 88])
        self.assertEqual(parts, [[7, 7, 3, 12], [41, 30, 24, 35], [65, 76, 59], [93, 97]])
        self.assertEqual(equal, [1, 1, 1])
        for seed in range(20):
            self.assertEqual(han_thorup_integer_sort(a, base=4, seed=seed), sorted(a))

    def test_edges(self):
        for a in ([], [1], [2, 1], [5] * 50, [0, -5, 3, -1, -5, 2], [2**63 - 1, -(2**63), 0, -1, 1] * 4):
            self.assertEqual(han_thorup_integer_sort(a, base=2, seed=1), sorted(a))

    def test_patterns(self):
        for a in (list(range(3000)), list(range(3000, 0, -1)), [i % 17 for i in range(3000)], [2**k for k in range(63)] * 5):
            self.assertEqual(han_thorup_integer_sort(a, seed=7), sorted(a))

    def test_bad_base(self):
        with self.assertRaises(ValueError):
            han_thorup_integer_sort([2, 1], base=1)

    def test_random(self):
        rng = random.Random(2002)
        for _ in range(600):
            n = rng.randint(0, 150)
            hi = rng.choice([3, 1000, 2**64])
            a = [rng.randint(-hi, hi) for _ in range(n)]
            got = han_thorup_integer_sort(a, base=rng.choice([2, 4, 8]), seed=rng.randrange(10**6))
            self.assertEqual(got, sorted(a))


if __name__ == "__main__":
    unittest.main()

What the next level needs

  • the tests did not run in this build

Read the full page about Han-Thorup randomized integer sorting