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
# 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