Algorithm Search⌘K

Cole's parallel merge sort in Python

Pipelined merge sort that sorts n keys in O(log n) time with n processors and O(n log n) work on a PRAM.

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

"""Cole's pipelined merge sort, simulated one stage at a time.

The tree has n = 2^k leaves, padded with items larger than all keys.
Node u is stored at index u of a heap layout, with children 2u and 2u + 1.
Every stage first forms SUP(v) for all live nodes from UP(v), and then
forms NEWUP(u) as the merge of SUP(2u) and SUP(2u + 1). Each item of
SUP(x) finds its rank in SUP(y) with at most three comparisons, by the
3-cover property. The ranks that Cole keeps up to date in O(1) time per
stage (line L9) are computed here by a sequential merge.
"""

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. Padding items are larger than all keys."""

    __slots__ = ("pad", "key", "index")

    def __init__(self, pad: bool, key: object, index: int) -> None:
        self.pad, self.key, self.index = pad, key, index

    def __lt__(self, other: "Item") -> bool:
        if self.pad != other.pad:
            return other.pad
        if not self.pad:
            if self.key < other.key:  # type: ignore[operator]
                return True
            if other.key < self.key:  # type: ignore[operator]
                return False
        return self.index < other.index


def rank_in(src: list[Item], dst: list[Item]) -> list[int]:
    """For each item of the sorted list src, the number of items of dst less than it."""
    out, j = [], 0
    for e in src:
        while j < len(dst) and dst[j] < e:
            j += 1
        out.append(j)
    return out


def cover_rank(e: Item, r: int, up: list[Item], up_in_y: list[int], sup_y: list[Item], stats: dict) -> int:
    """Rank of e in sup_y, from the rank r of e in up and the ranks of up in sup_y."""
    lo = up_in_y[r - 1] if r > 0 else 0  # @line L6
    hi = up_in_y[r] if r < len(up) else len(sup_y)
    stats["max_candidates"] = max(stats["max_candidates"], hi - lo)
    j = lo
    while j < hi and sup_y[j] < e:  # @line L7
        j += 1
    return j


def cole_merge_sort(a: list[T], stats: dict | None = None) -> list[T]:
    """Return a stably sorted copy of a."""
    if stats is None:
        stats = {}
    stats.update(max_candidates=0, stages=0, max_live=0)
    n = len(a)
    if n <= 1:
        return list(a)
    k = (n - 1).bit_length()
    size = 1 << k
    up: dict[int, list[Item]] = {}
    full_at: dict[int, int] = {}
    for i in range(size):  # @line L1
        up[size + i] = [Item(i >= n, a[i] if i < n else None, i)]
        full_at[size + i] = 0
    for t in range(1, 3 * k + 1):  # @line L2
        stats["stages"] = t
        sup: dict[int, list[Item]] = {}
        for v, items in up.items():  # @line L3
            if v not in full_at:
                step = 4  # @line L4
            else:
                age = t - full_at[v]
                if age > 3:
                    continue
                step = {1: 4, 2: 2, 3: 1}[age]  # @line L5
            sup[v] = items[step - 1 :: step]
        live = sum(len(s) for s in sup.values())
        new_up: dict[int, list[Item]] = {}
        for u in range(1, size):
            x, y = 2 * u, 2 * u + 1
            sx, sy = sup.get(x, []), sup.get(y, [])
            if u in full_at or not (sx or sy):
                continue
            cur = up.get(u, [])
            # Ranks that Cole maintains in O(1) time per stage.  # @line L9
            x_in_up, y_in_up = rank_in(sx, cur), rank_in(sy, cur)
            up_in_x, up_in_y = rank_in(cur, sx), rank_in(cur, sy)
            merged: list[Item | None] = [None] * (len(sx) + len(sy))
            for i, e in enumerate(sx):
                merged[i + cover_rank(e, x_in_up[i], cur, up_in_y, sy, stats)] = e  # @line L8
            for j, e in enumerate(sy):
                merged[j + cover_rank(e, y_in_up[j], cur, up_in_x, sx, stats)] = e
            new_up[u] = merged  # type: ignore[assignment]
        for u, items in new_up.items():  # @line L10
            up[u] = items
            live += len(items)
            height = k - (u.bit_length() - 1)
            if len(items) == 1 << height:
                full_at[u] = t
        stats["max_live"] = max(stats["max_live"], live)
    return [it.key for it in up[1] if not it.pad]  # @line L11
test_cole_merge_sort.py 54 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/cole-merge-sort/. The authors are listed in prov/342B4P.jsonl.

import itertools
import random

from cole_merge_sort import cole_merge_sort


def test_example() -> None:
    stats: dict = {}
    a = [42, 7, 93, 25, 60, 12, 88, 3]
    assert cole_merge_sort(a, stats) == [3, 7, 12, 25, 42, 60, 88, 93]
    assert stats["stages"] == 9


def test_edges() -> None:
    assert cole_merge_sort([]) == []
    assert cole_merge_sort([5]) == [5]
    assert cole_merge_sort([2, 1]) == [1, 2]
    assert cole_merge_sort([3, 1, 2]) == [1, 2, 3]


def test_all_permutations() -> None:
    for n in range(1, 8):
        for perm in itertools.permutations(range(n)):
            assert cole_merge_sort(list(perm)) == list(range(n))


def test_random_and_cover() -> None:
    rng = random.Random(3)
    for n in [5, 16, 31, 64, 100, 256, 512]:
        a = [rng.randrange(n) for _ in range(n)]
        stats: dict = {}
        assert cole_merge_sort(a, stats) == sorted(a)
        assert stats["max_candidates"] <= 3
        k = (n - 1).bit_length()
        assert stats["stages"] == 3 * k
        assert stats["max_live"] <= 4 * (1 << k)


def test_stable() -> None:
    rng = random.Random(5)
    pairs = [(rng.randrange(4), i) for i in range(50)]

    class K:
        def __init__(self, p: tuple[int, int]) -> None:
            self.p = p

        def __lt__(self, other: "K") -> bool:
            return self.p[0] < other.p[0]

    out = cole_merge_sort([K(p) for p in pairs])
    assert [k.p for k in out] == sorted(pairs, key=lambda p: p[0])

What the next level needs

  • the tests did not run in this build

Read the full page about Cole's parallel merge sort