MSD radix sort in Python
Partitions keys by their leading digit, then recursively sorts each bucket on the next digit.
- File
msd_radix_sort.py, 43 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/msd-radix-sort/. The authors are listed in prov/5YMPGZ.jsonl.
def _c(s: str, d: int) -> int:
"""The bucket 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
def msd_radix_sort(a: list[str], r: int = 256) -> 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. The sort is stable.
The cost is O(r (n + D)) time, where D is the total length of the
distinguishing prefixes, and Theta(n + r) extra space for the buffer.
An explicit stack replaces the recursion, so long common prefixes are safe.
"""
for s in a:
for ch in s:
if ord(ch) >= r:
raise ValueError("a character is outside the alphabet")
stack = [(0, len(a), 0)] # @line L1
while stack: # @line L2
lo, hi, d = stack.pop() # @line L3
if hi - lo <= 1: # @line L4
continue
count = [0] * (r + 1) # @line L5
for i in range(lo, hi):
count[_c(a[i], d)] += 1
start = [0] * (r + 2) # @line L6
for v in range(r + 1):
start[v + 1] = start[v] + count[v]
nxt = start[:]
buf = [""] * (hi - lo)
for i in range(lo, hi): # @line L7
v = _c(a[i], d)
buf[nxt[v]] = a[i]
nxt[v] += 1
a[lo:hi] = buf # @line L8
for v in range(r, 0, -1): # @line L9
if start[v + 1] - start[v] >= 2:
stack.append((lo + start[v], lo + start[v + 1], d + 1)) # @line L10
return a # @line L11
test_msd_radix_sort.py 32 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/msd-radix-sort/. The authors are listed in prov/5YMPGZ.jsonl.
import random
from msd_radix_sort import msd_radix_sort
def test_example() -> None:
a = ["she", "sells", "sea", "shells", "by", "the", "sea", "shore"]
assert msd_radix_sort(a) == ["by", "sea", "sea", "sells", "she", "shells", "shore", "the"]
def test_edges() -> None:
assert msd_radix_sort([]) == []
assert msd_radix_sort(["x"]) == ["x"]
assert msd_radix_sort(["", "a", ""]) == ["", "", "a"]
assert msd_radix_sort(["ab", "a", "abc", "a"]) == ["a", "a", "ab", "abc"]
assert msd_radix_sort(["b", "a"], r=128) == ["a", "b"]
def test_long_common_prefix() -> None:
p = "q" * 5000
a = [p + "b", p, p + "a", p]
assert msd_radix_sort(a) == sorted(a)
def test_random_against_sorted() -> None:
rng = random.Random(3301)
for _ in range(600):
a = ["".join(rng.choice("abc") for _ in range(rng.randint(0, 6))) for _ in range(rng.randint(0, 30))]
assert msd_radix_sort(list(a)) == sorted(a)
What the next level needs
- the tests did not run in this build