Algorithm Search⌘K

Treap merge in Python

Joins two treaps where all keys of one precede the other into a single treap, following heap priorities, in expected logarithmic time.

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

from __future__ import annotations

from typing import Generic, Iterator, Protocol, TypeVar


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


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


class Node(Generic[K]):
    __slots__ = ("key", "pri", "left", "right")

    def __init__(self, key: K, pri: float) -> None:
        self.key = key
        self.pri = pri
        self.left: Node[K] | None = None
        self.right: Node[K] | None = None


def build(pairs: list[tuple[K, float]]) -> Node[K] | None:
    """Build the treap of distinct (key, priority) pairs.

    Sorts by key and keeps the right spine on a stack, as for a Cartesian
    tree. O(n log n) for the sort, O(n) after it.
    """
    spine: list[Node[K]] = []
    for k, p in sorted(pairs, key=lambda kp: kp[0]):
        x = Node(k, p)
        last: Node[K] | None = None
        while spine and spine[-1].pri < p:
            last = spine.pop()
        x.left = last
        if spine:
            spine[-1].right = x
        spine.append(x)
    return spine[0] if spine else None


def keys(t: Node[K] | None) -> Iterator[K]:
    """Yield the keys of t in increasing order. O(size of t)."""
    stack: list[Node[K]] = []
    while stack or t is not None:
        while t is not None:
            stack.append(t)
            t = t.left
        t = stack.pop()
        yield t.key
        t = t.right


def check_treap(t: Node[K] | None) -> int:
    """Raise AssertionError unless t has search order on keys and max-heap
    order on priorities; return its number of nodes."""
    count = 0
    stack: list[tuple[Node[K], K | None, K | None]] = [(t, None, None)] if t else []
    while stack:
        x, lo, hi = stack.pop()
        count += 1
        assert lo is None or lo < x.key
        assert hi is None or x.key < hi
        if x.left is not None:
            assert x.left.pri <= x.pri
            stack.append((x.left, lo, x.key))
        if x.right is not None:
            assert x.right.pri <= x.pri
            stack.append((x.right, x.key, hi))
    return count


def merge(a: Node[K] | None, b: Node[K] | None) -> Node[K] | None:
    """Merge two treaps into one, when every key of a is less than every
    key of b. The nodes are reused.

    Precondition: a and b are treaps with keys(a) < keys(b). Cost
    O(length of the right spine of a + length of the left spine of b),
    which is O(log n) expected for random priorities.
    """
    if a is None:  # @line L1
        return b
    if b is None:  # @line L2
        return a
    if a.pri > b.pri:  # @line L3
        a.right = merge(a.right, b)  # @line L4
        return a  # @line L5
    b.left = merge(a, b.left)  # @line L6
    return b  # @line L7
test_treap_merge.py 59 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/treap-merge/. The authors are listed in prov/J8TN0B.jsonl.

import random

import pytest

from treap_merge import Node, build, check_treap, keys, merge


def shape(t: "Node[int] | None") -> object:
    if t is None:
        return None
    if t.left is None and t.right is None:
        return t.key
    return (shape(t.left), t.key, shape(t.right))


def test_page_example() -> None:
    a = build([(10, 20.0), (20, 10.0), (30, 60.0), (40, 80.0), (50, 30.0)])
    b = build([(60, 50.0), (70, 40.0), (80, 70.0)])
    assert shape(a) == (((None, 10, 20), 30, None), 40, 50)
    assert shape(b) == ((None, 60, 70), 80, None)
    t = merge(a, b)
    assert shape(t) == (((None, 10, 20), 30, None), 40, ((50, 60, 70), 80, None))
    assert check_treap(t) == 8


def test_edge_cases() -> None:
    assert merge(None, None) is None
    b = build([(1, 0.5)])
    assert merge(None, b) is b
    assert merge(b, None) is b
    # equal priorities are allowed
    t = merge(build([(1, 0.5), (2, 0.5)]), build([(3, 0.5)]))
    check_treap(t)
    assert list(keys(t)) == [1, 2, 3]


def test_overlapping_keys_break_the_order() -> None:
    # merge needs every key of a below every key of b
    a = build([(1, 0.9), (5, 0.1)])
    b = build([(3, 0.5)])
    t = merge(a, b)
    with pytest.raises(AssertionError):
        check_treap(t)


def test_random_against_sets() -> None:
    rng = random.Random(1996)
    for _ in range(600):
        ks = sorted(rng.sample(range(200), rng.randrange(0, 60)))
        cut = rng.randrange(len(ks) + 1)
        left, right = ks[:cut], ks[cut:]
        a = build([(k, rng.random()) for k in left])
        b = build([(k, rng.random()) for k in right])
        t = merge(a, b)
        assert list(keys(t)) == ks
        assert check_treap(t) == len(ks)

What the next level needs

  • the tests did not run in this build

Read the full page about Treap merge