Treap split in Python
Splits a treap by key or position into two treaps, one holding smaller elements and one holding the rest, in expected logarithmic time.
- File
treap_split.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-split/. The authors are listed in prov/WYJ3AW.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 split(t: Node[K] | None, k: K) -> tuple[Node[K] | None, Node[K] | None]:
"""Split the treap t into (L, R): the keys less than k go to L, the
keys at least k go to R. Both are treaps. The nodes of t are reused.
Precondition: t is a treap. Cost O(depth of the search path of k),
which is O(log n) expected for random priorities.
"""
if t is None: # @line L1
return None, None
if t.key < k: # @line L2
a, b = split(t.right, k) # @line L3
t.right = a # @line L4
return t, b # @line L5
# @line L6
a, b = split(t.left, k) # @line L7
t.left = b # @line L8
return a, t # @line L9
test_treap_split.py 87 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/treap-split/. The authors are listed in prov/WYJ3AW.jsonl.
import random
from treap_split import Node, build, check_treap, keys, split
PAIRS = [(10, 20), (20, 10), (30, 60), (40, 80), (50, 30), (60, 50), (70, 40), (80, 70)]
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:
t = build([(k, float(p)) for k, p in PAIRS])
assert shape(t) == (((None, 10, 20), 30, None), 40, ((50, 60, 70), 80, None))
a, b = split(t, 55)
assert shape(a) == (((None, 10, 20), 30, None), 40, 50)
assert shape(b) == ((None, 60, 70), 80, None)
check_treap(a)
check_treap(b)
def test_edge_cases() -> None:
assert split(None, 5) == (None, None)
t = build([(k, float(p)) for k, p in PAIRS])
a, b = split(t, 0)
assert a is None and list(keys(b)) == [k for k, _ in sorted(PAIRS)]
t = build([(k, float(p)) for k, p in PAIRS])
a, b = split(t, 99)
assert b is None and list(keys(a)) == [k for k, _ in sorted(PAIRS)]
def test_equal_key_goes_right() -> None:
# keys less than k go left; a key equal to k goes right
t = build([(k, float(p)) for k, p in PAIRS])
a, b = split(t, 40)
assert list(keys(a)) == [10, 20, 30]
assert list(keys(b)) == [40, 50, 60, 70, 80]
assert b is not None and b.key == 40
def test_random_against_sets() -> None:
rng = random.Random(1989)
for _ in range(600):
n = rng.randrange(0, 50)
ks = rng.sample(range(100), n)
t = build([(k, rng.random()) for k in ks])
k = rng.randrange(-1, 102)
a, b = split(t, k)
assert list(keys(a)) == sorted(x for x in ks if x < k)
assert list(keys(b)) == sorted(x for x in ks if x >= k)
assert check_treap(a) + check_treap(b) == n
def test_expected_path_length() -> None:
# split visits one node for each level of the search path of k
rng = random.Random(5)
n = 4096
total = 0
visits = [0]
def counting_split(t: "Node[int] | None", k: int) -> tuple:
if t is None:
return None, None
visits[0] += 1
if t.key < k:
a, b = counting_split(t.right, k)
t.right = a
return t, b
a, b = counting_split(t.left, k)
t.left = b
return a, t
trials = 50
for _ in range(trials):
t = build([(k, rng.random()) for k in range(n)])
visits[0] = 0
counting_split(t, rng.randrange(n))
total += visits[0]
# about 2 ln n, which is less than 17 for n = 4096
assert total / trials < 3 * 8.32
What the next level needs
- the tests did not run in this build