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