Algorithm Search⌘K

Parallel radix sort in Python

Radix sort whose per-digit counting and scatter steps use parallel prefix sums.

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

"""Parallel LSD radix sort with p blocks, simulated one block at a time.

Each pass counts digits per block, takes one prefix sum over the count
table in digit-major, block-minor order, and lets every block scatter
its keys to their final places. The blocks touch disjoint cells, so
running them one after the other gives the same result as running them
at the same time.
"""


def block_bounds(n: int, p: int, j: int) -> tuple[int, int]:
    """The index range of block j when n keys are cut into p blocks."""
    return j * n // p, (j + 1) * n // p


def digit(x: int, t: int, radix: int) -> int:
    """Digit t of x in base radix, with digit 0 the least significant."""
    return (x // radix**t) % radix


def radix_pass(a: list[int], t: int, radix: int, p: int) -> list[int]:
    """One stable counting-sort pass on digit t, with p blocks."""
    n = len(a)
    counts = [[0] * p for _ in range(radix)]
    for j in range(p):  # @line L2
        lo, hi = block_bounds(n, p, j)
        for i in range(lo, hi):  # @line L3
            counts[digit(a[i], t, radix)][j] += 1
    offset = [[0] * p for _ in range(radix)]
    total = 0
    for d in range(radix):  # @line L4
        for j in range(p):
            offset[d][j] = total
            total += counts[d][j]
    out = [0] * n
    for j in range(p):  # @line L5
        lo, hi = block_bounds(n, p, j)
        for i in range(lo, hi):  # @line L6
            d = digit(a[i], t, radix)  # @line L7
            out[offset[d][j]] = a[i]
            offset[d][j] += 1
    return out  # @line L8


def parallel_radix_sort(a: list[int], radix: int = 256, p: int = 4) -> list[int]:
    """Return the non-negative integers of a in sorted order."""
    if any(x < 0 for x in a):
        raise ValueError("keys must be non-negative")
    if radix < 2 or p < 1:
        raise ValueError("need radix >= 2 and p >= 1")
    passes = 1
    while a and radix**passes <= max(a):
        passes += 1
    out = list(a)
    for t in range(passes):  # @line L1
        out = radix_pass(out, t, radix, p)
    return out


def split_radix_sort(a: list[int], bits: int) -> list[int]:
    """Blelloch's split radix sort: one stable split by each bit, low bit first."""
    out = list(a)
    for t in range(bits):
        flags = [(x >> t) & 1 for x in out]
        zeros = len(out) - sum(flags)
        ones_before, pos = 0, [0] * len(out)
        for i, f in enumerate(flags):  # a prefix sum of the flags
            pos[i] = zeros + ones_before if f else i - ones_before
            ones_before += f
        nxt = [0] * len(out)
        for i, x in enumerate(out):
            nxt[pos[i]] = x
        out = nxt
    return out
test_parallel_radix_sort.py 54 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/parallel-radix-sort/. The authors are listed in prov/GHWWJ6.jsonl.

import random

import pytest

from parallel_radix_sort import parallel_radix_sort, radix_pass, split_radix_sort


EXAMPLE = [170, 45, 75, 90, 802, 24, 2, 66]


def test_example_passes() -> None:
    first = radix_pass(EXAMPLE, 0, 10, 2)
    assert first == [170, 90, 802, 2, 24, 45, 75, 66]
    second = radix_pass(first, 1, 10, 2)
    assert second == [802, 2, 24, 45, 66, 170, 75, 90]
    assert radix_pass(second, 2, 10, 2) == sorted(EXAMPLE)


def test_example() -> None:
    assert parallel_radix_sort(EXAMPLE, 10, 2) == sorted(EXAMPLE)
    assert split_radix_sort(EXAMPLE, 10) == sorted(EXAMPLE)


def test_edges() -> None:
    assert parallel_radix_sort([]) == []
    assert parallel_radix_sort([0]) == [0]
    assert parallel_radix_sort([3, 3, 3], 2, 5) == [3, 3, 3]
    with pytest.raises(ValueError):
        parallel_radix_sort([-1])


def test_pass_is_stable() -> None:
    a = [13, 23, 3, 41, 11, 33, 1]
    assert radix_pass(a, 0, 10, 3) == [41, 11, 1, 13, 23, 3, 33]


def test_block_count_does_not_change_result() -> None:
    rng = random.Random(4)
    a = [rng.randrange(10**6) for _ in range(500)]
    want = sorted(a)
    for p in (1, 2, 3, 7, 64, 500, 600):
        for radix in (2, 10, 16, 256):
            assert parallel_radix_sort(a, radix, p) == want


def test_random_against_sorted() -> None:
    rng = random.Random(12)
    for _ in range(200):
        a = [rng.randrange(1 << 16) for _ in range(rng.randint(0, 60))]
        assert parallel_radix_sort(a, 16, rng.randint(1, 9)) == sorted(a)
        assert split_radix_sort(a, 16) == sorted(a)

What the next level needs

  • the tests did not run in this build

Read the full page about Parallel radix sort