Algorithm Search⌘K

Burstsort in Python

Cache-efficient string sort that builds a trie of buckets which burst into subtries as they grow.

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


def _c(s: str, d: int) -> int:
    """The slot of s at depth d: 0 when s has ended, else the code of s[d] plus 1."""
    return ord(s[d]) + 1 if d < len(s) else 0


class _Node:
    """A trie node: each slot is None, a bucket (list of str) or a child _Node."""

    __slots__ = ("slots",)

    def __init__(self, r: int) -> None:
        self.slots: list = [None] * (r + 1)


def _burst(v: _Node, r: int, d: int, radix: int) -> None:
    u = _Node(radix)
    for s in v.slots[r]:
        k = _c(s, d)
        if u.slots[k] is None:
            u.slots[k] = []
        u.slots[k].append(s)
    v.slots[r] = u


def _mkq(a: list[str], d0: int, rng: random.Random) -> None:
    """Multikey quicksort of a, in place; all strings agree on d0 characters."""
    stack = [(0, len(a), d0)]
    while stack:
        lo, hi, d = stack.pop()
        if hi - lo <= 1:
            continue
        v = _c(a[rng.randrange(lo, hi)], d)
        lt, i, gt = lo, lo, hi - 1
        while i <= gt:
            x = _c(a[i], d)
            if x < v:
                a[lt], a[i] = a[i], a[lt]
                lt += 1
                i += 1
            elif x > v:
                a[i], a[gt] = a[gt], a[i]
                gt -= 1
            else:
                i += 1
        gt += 1
        stack.append((gt, hi, d))
        if v != 0:
            stack.append((lt, gt, d + 1))
        stack.append((lo, lt, d))


def burstsort(a: list[str], threshold: int = 64, r: int = 256, seed: int | None = None) -> list[str]:
    """Sort the list a of strings in lexicographic order, in place, and return it.

    Precondition: each character has a code less than r, threshold >= 1.
    The cost is O(n log T + D + r (n + D) / T) expected time, where D is the
    total length of the distinguishing prefixes, and O(n + r (n + D) / T) space.
    """
    if threshold < 1:
        raise ValueError("threshold must be at least 1")
    for s in a:
        for ch in s:
            if ord(ch) >= r:
                raise ValueError("a character is outside the alphabet")
    rng = random.Random(seed)
    root = _Node(r)  # @line L1
    for s in a:  # @line L2
        v, d = root, 0  # @line L3
        while isinstance(v.slots[_c(s, d)], _Node):  # @line L4
            v = v.slots[_c(s, d)]
            d += 1
        k = _c(s, d)  # @line L5
        if v.slots[k] is None:
            v.slots[k] = []
        v.slots[k].append(s)
        if k != 0 and len(v.slots[k]) > threshold:  # @line L6
            _burst(v, k, d + 1, r)
    pos = 0  # @line L7
    stack: list[tuple[_Node, int, int]] = [(root, 0, 0)]
    while stack:  # @line L8
        v, k, d = stack.pop()
        if k > r:
            continue
        stack.append((v, k + 1, d))
        item = v.slots[k]
        if item is None:
            continue
        if isinstance(item, _Node):
            stack.append((item, 0, d + 1))
            continue
        if k != 0:  # @line L9
            _mkq(item, d + 1, rng)
        a[pos:pos + len(item)] = item  # @line L10
        pos += len(item)
    return a  # @line L11
test_burstsort.py 46 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/burstsort/. The authors are listed in prov/QN966N.jsonl.
import random
import unittest

from burstsort import burstsort


class TestBurstsort(unittest.TestCase):
    def test_page_example(self):
        a = "she sells sea shells by the sea shore".split()
        self.assertEqual(burstsort(a, threshold=2), ["by", "sea", "sea", "sells", "she", "shells", "shore", "the"])

    def test_in_place(self):
        a = ["b", "a"]
        self.assertIs(burstsort(a), a)
        self.assertEqual(a, ["a", "b"])

    def test_edges(self):
        self.assertEqual(burstsort([]), [])
        self.assertEqual(burstsort(["x"]), ["x"])
        self.assertEqual(burstsort(["", "", "a", ""], threshold=1), ["", "", "", "a"])
        self.assertEqual(burstsort(["dup"] * 20, threshold=1), ["dup"] * 20)
        self.assertEqual(burstsort(["ab", "b", "abc", "a", "ba"], threshold=1), ["a", "ab", "abc", "b", "ba"])
        with self.assertRaises(ValueError):
            burstsort(["a"], threshold=0)
        with self.assertRaises(ValueError):
            burstsort(["Ā"])

    def test_long_common_prefix(self):
        p = "u" * 300
        a = [p + c for c in "zyxwvutsrq"] + [p]
        self.assertEqual(burstsort(list(a), threshold=2), sorted(a))

    def test_random(self):
        rng = random.Random(2003)
        for _ in range(600):
            n = rng.randint(0, 60)
            k = rng.choice([1, 2, 3, 26])
            a = ["".join(chr(97 + rng.randrange(k)) for _ in range(rng.randint(0, 6))) for _ in range(n)]
            t = rng.choice([1, 2, 4, 64])
            self.assertEqual(burstsort(list(a), threshold=t, seed=rng.randrange(1000)), 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 Burstsort