Algorithm Search⌘K

Parallel sorting by regular sampling in Python

Each processor sorts its block, takes regularly spaced samples to choose splitters, then merges the pieces.

File
parallel_sorting_by_regular_sampling.py, 62 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
parallel_sorting_by_regular_sampling.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/parallel-sorting-by-regular-sampling/. The authors are listed in prov/6H7AM2.jsonl.

"""Parallel sorting by regular sampling (PSRS) on p simulated processors.

Each processor's work is done in turn. Keys are tagged with their input
index, so all items are distinct and the sort is stable.
"""

import bisect
import heapq
from typing import Protocol, TypeVar


class Ordered(Protocol):
    def __lt__(self, other: "Ordered", /) -> bool: ...


T = TypeVar("T", bound=Ordered)


class Item:
    """A key with its input index, ordered by key and then by index."""

    __slots__ = ("key", "index")

    def __init__(self, key: Ordered, index: int) -> None:
        self.key, self.index = key, index

    def __lt__(self, other: "Item") -> bool:
        if self.key < other.key:
            return True
        if other.key < self.key:
            return False
        return self.index < other.index


def psrs(a: list[T], p: int) -> tuple[list[T], list[int]]:
    """Return a stably sorted copy of a, and the number of keys on each processor."""
    if p < 1:
        raise ValueError("need p >= 1")
    n = len(a)
    items = [Item(x, i) for i, x in enumerate(a)]
    blocks = [sorted(items[b * n // p : (b + 1) * n // p]) for b in range(p)]  # @line L1
    samples: list[Item] = []
    for blk in blocks:  # @line L2
        samples.extend(blk[t * len(blk) // p] for t in range(p) if blk)
    samples.sort()  # @line L3
    q = len(samples) // p
    pivots = [samples[j * q - 1] for j in range(1, p)] if q else []
    pieces: list[list[list[Item]]] = [[] for _ in range(p)]
    for blk in blocks:  # @line L4
        cuts = [0] + [bisect.bisect_right(blk, y) for y in pivots] + [len(blk)]
        for j in range(len(cuts) - 1):  # @line L5: piece j goes to processor j
            pieces[j].append(blk[cuts[j] : cuts[j + 1]])
    out: list[T] = []
    sizes = []
    for j in range(p):  # @line L6
        merged = list(heapq.merge(*pieces[j])) if pieces[j] else []
        sizes.append(len(merged))
        out.extend(it.key for it in merged)
    return out, sizes
test_parallel_sorting_by_regular_sampling.py 64 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/parallel-sorting-by-regular-sampling/. The authors are listed in prov/6H7AM2.jsonl.

import random

import pytest

from parallel_sorting_by_regular_sampling import psrs


EXAMPLE = [15, 46, 48, 93, 39, 6, 72, 91, 14, 36, 69, 40, 89, 61, 97, 12, 21, 54, 53, 97, 84, 58, 32, 27, 33, 72, 20]


def test_example() -> None:
    out, sizes = psrs(EXAMPLE, 3)
    assert out == sorted(EXAMPLE)
    assert sizes == [5, 7, 15]


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


def test_bucket_bound_distinct_keys() -> None:
    rng = random.Random(4)
    for p in (2, 3, 4, 5, 8):
        n = p * p * rng.randint(1, 6)
        for _ in range(200):
            a = rng.sample(range(10 * n), n)
            out, sizes = psrs(a, p)
            assert out == sorted(a)
            assert max(sizes) <= 2 * n // p - p


def test_equal_keys_are_balanced_by_tags() -> None:
    out, sizes = psrs([5] * 64, 4)
    assert out == [5] * 64
    assert max(sizes) <= 2 * 64 // 4 - 4


def test_stable() -> None:
    class Key:
        def __init__(self, k: int, tag: int) -> None:
            self.k, self.tag = k, tag

        def __lt__(self, other: "Key") -> bool:
            return self.k < other.k

    rng = random.Random(9)
    a = [Key(rng.randint(0, 3), i) for i in range(100)]
    out, _ = psrs(a, 4)
    assert [(x.k, x.tag) for x in out] == sorted((x.k, x.tag) for x in a)


def test_random_any_size() -> None:
    rng = random.Random(5)
    for _ in range(300):
        a = [rng.randint(-30, 30) for _ in range(rng.randint(0, 70))]
        out, _ = psrs(a, rng.randint(1, 9))
        assert out == sorted(a)

What the next level needs

  • the tests did not run in this build

Read the full page about Parallel sorting by regular sampling