Algorithm Search⌘K

Han's integer sorting in Python

Deterministic word-RAM algorithm that sorts n integers of w bits in O(n log log n) time.

File
han_integer_sort.py, 56 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
han_integer_sort.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/han-integer-sort/. The authors are listed in prov/0V80XQ.jsonl.
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_integer_sort(a: list[int], base: int = 8) -> 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 Han's algorithm: a recursion on about sqrt(n) splitters,
    as in an exponential search tree, with a deterministic sample. Han passes
    the keys down each level in O(1) amortized time per key with packed words,
    which gives O(n log log n) time in total. Here each key is placed by binary
    search, so one level costs O(n log n), and the sample is not balanced on
    every input. The extra space is O(n).
    """
    if base < 2:
        raise ValueError("base must be at least 2")
    return _sort(list(a), base)


def _sort(a: list[int], base: int) -> list[int]:
    n = len(a)
    if n <= base:  # @line L1
        return _insertion_sort(a)
    k = isqrt(n - 1) + 1  # @line L2
    sample = a[::k]  # @line L3
    p = _sort(sample, base)  # @line L4
    splitters = [x for i, x in enumerate(p) if i == 0 or x != p[i - 1]]
    s = len(splitters)
    parts: list[list[int]] = [[] for _ in range(s + 1)]
    equal = [0] * s
    for x in a:  # @line L5
        j = bisect_left(splitters, x)
        if j < s and splitters[j] == x:  # @line L6
            equal[j] += 1
        else:
            parts[j].append(x)
    out: list[int] = []
    for j in range(s + 1):  # @line L7
        out.extend(_sort(parts[j], base))
        if j < s:
            out.extend([splitters[j]] * equal[j])
    return out  # @line L8
test_han_integer_sort.py 38 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/han-integer-sort/. The authors are listed in prov/0V80XQ.jsonl.
import random
import unittest

from han_integer_sort import han_integer_sort


class TestHanIntegerSort(unittest.TestCase):
    def test_page_example(self):
        a = [41, 7, 93, 18, 65, 30, 7, 88, 51, 24, 76, 3, 59, 12, 97, 35]
        self.assertEqual(a[::4], [41, 65, 51, 59])
        self.assertEqual(han_integer_sort(a, base=4), sorted(a))
        self.assertEqual(a[0], 41)

    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_integer_sort(a, base=2), 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_integer_sort(a), sorted(a))

    def test_bad_base(self):
        with self.assertRaises(ValueError):
            han_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)]
            self.assertEqual(han_integer_sort(a, base=rng.choice([2, 4, 8])), 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's integer sorting