Algorithm Search⌘K

Parallel merge sort in Python

Merge sort in which the two recursive calls and the merge run in parallel.

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

"""Parallel merge sort in the work-span model.

The code runs the spawned calls one after the other, which is a legal
schedule of the parallel program. Each function also returns the work
and the span of the computation in a unit-cost model: one unit for each
call, each probe of a binary search and each copied item.
"""

from typing import Protocol, TypeVar


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


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


def lower_bound(y: list[T], lo: int, hi: int, key: T) -> tuple[int, int]:
    """First index r in [lo, hi] with not y[r] < key, and the number of probes."""
    probes = 0
    while lo < hi:
        probes += 1
        mid = (lo + hi) // 2
        if y[mid] < key:
            lo = mid + 1
        else:
            hi = mid
    return lo, probes


def upper_bound(x: list[T], lo: int, hi: int, key: T) -> tuple[int, int]:
    """First index r in [lo, hi] with key < x[r], and the number of probes."""
    probes = 0
    while lo < hi:
        probes += 1
        mid = (lo + hi) // 2
        if key < x[mid]:
            hi = mid
        else:
            lo = mid + 1
    return lo, probes


def p_merge(x: list[T], a1: int, a2: int, y: list[T], b1: int, b2: int, z: list[T], c: int) -> tuple[int, int]:
    """Merge sorted x[a1:a2] and y[b1:b2] into z[c:], stably (x first on ties).

    Return (work, span)."""
    na, nb = a2 - a1, b2 - b1  # @line L8
    if na + nb == 0:  # @line L9
        return 1, 1
    if na >= nb:  # @line L10
        q = a1 + na // 2  # @line L11
        r, probes = lower_bound(y, b1, b2, x[q])  # @line L12
        t = c + (q - a1) + (r - b1)  # @line L13
        z[t] = x[q]  # @line L14
        w1, s1 = p_merge(x, a1, q, y, b1, r, z, c)  # @line L15
        w2, s2 = p_merge(x, q + 1, a2, y, r, b2, z, t + 1)  # @line L16
        # sync: the two calls above are done here  # @line L17
    else:  # @line L18
        q = b1 + nb // 2  # @line L19
        r, probes = upper_bound(x, a1, a2, y[q])  # @line L20
        t = c + (r - a1) + (q - b1)  # @line L21
        z[t] = y[q]  # @line L22
        w1, s1 = p_merge(x, a1, r, y, b1, q, z, c)  # @line L23
        w2, s2 = p_merge(x, r, a2, y, q + 1, b2, z, t + 1)  # @line L24
        # sync: the two calls above are done here  # @line L25
    return 1 + probes + w1 + w2, 1 + probes + max(s1, s2)


def p_merge_sort(a: list[T], lo: int = 0, hi: int | None = None, buf: list[T] | None = None) -> tuple[int, int]:
    """Sort a[lo:hi] in place, stably. Return (work, span)."""
    if hi is None:
        hi = len(a)
    if buf is None:
        buf = list(a)
    if hi - lo <= 1:  # @line L1
        return 1, 1
    mid = lo + (hi - lo) // 2  # @line L2
    w1, s1 = p_merge_sort(a, lo, mid, buf)  # @line L3
    w2, s2 = p_merge_sort(a, mid, hi, buf)  # @line L4
    # sync: both halves are sorted here  # @line L5
    buf[lo:hi] = a[lo:hi]  # @line L6: a parallel loop, span about lg(hi - lo)
    copy_span = (hi - lo).bit_length()
    w3, s3 = p_merge(buf, lo, mid, buf, mid, hi, a, lo)  # @line L7
    return 1 + w1 + w2 + (hi - lo) + w3, 1 + max(s1, s2) + copy_span + s3
test_parallel_merge_sort.py 62 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/parallel-merge-sort/. The authors are listed in prov/J1JZCG.jsonl.

import math
import random

from parallel_merge_sort import p_merge, p_merge_sort


def test_example() -> None:
    a = [38, 27, 43, 3, 9, 82, 10]
    p_merge_sort(a)
    assert a == [3, 9, 10, 27, 38, 43, 82]


def test_merge_example() -> None:
    x, y = [3, 27, 38, 43], [9, 10, 82]
    z = [0] * 7
    p_merge(x, 0, 4, y, 0, 3, z, 0)
    assert z == [3, 9, 10, 27, 38, 43, 82]


def test_edges() -> None:
    for a in ([], [1], [2, 1], [1, 1, 1], [3, 2, 1], [1, 2, 3]):
        b = list(a)
        p_merge_sort(b)
        assert b == sorted(a)


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

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

    a = [Key(2, "a"), Key(1, "b"), Key(2, "c"), Key(1, "d"), Key(2, "e"), Key(1, "f")]
    p_merge_sort(a)
    assert [x.tag for x in a] == ["b", "d", "f", "a", "c", "e"]


def test_merge_work_is_linear_and_span_polylog() -> None:
    rng = random.Random(3)
    for n in (1, 2, 5, 64, 1000, 4096):
        keys = sorted(rng.sample(range(10 * n + 10), 2 * n))
        x, y = keys[0::2], keys[1::2]
        z = [0] * (2 * n)
        work, span = p_merge(x, 0, n, y, 0, n, z, 0)
        assert z == keys
        big = 2 * n
        assert work <= 8 * big + 2
        assert span <= (math.log(big, 4 / 3) + 2) * (math.log2(big) + 2)


def test_random_against_sorted() -> None:
    rng = random.Random(11)
    for _ in range(300):
        a = [rng.randint(-30, 30) for _ in range(rng.randint(0, 70))]
        b = list(a)
        p_merge_sort(b)
        assert b == sorted(a)

What the next level needs

  • the tests did not run in this build

Read the full page about Parallel merge sort