Treap in Python
A binary search tree in which each node also has a random priority and the tree is kept heap ordered by priority, giving expected logarithmic height.
- File
treap.py, 178 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/. The authors are listed in prov/S6DS4V.jsonl.
from __future__ import annotations
import random
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 rotate_right(t: Node[K]) -> Node[K]:
"""Lift t.left above t. O(1)."""
x = t.left
assert x is not None
t.left = x.right
x.right = t
return x
def rotate_left(t: Node[K]) -> Node[K]:
"""Lift t.right above t. O(1)."""
x = t.right
assert x is not None
t.right = x.left
x.left = t
return x
def insert(t: Node[K] | None, k: K, p: float) -> Node[K]:
"""Insert key k with priority p below t; return the new subtree root.
Precondition: t is a treap (search order on keys, max-heap order on
priorities). Cost O(depth of k).
"""
if t is None: # @line L1
return Node(k, p)
if k < t.key: # @line L2
t.left = insert(t.left, k, p)
if t.left.pri > t.pri: # @line L3
t = rotate_right(t)
elif t.key < k: # @line L4
t.right = insert(t.right, k, p)
if t.right.pri > t.pri: # @line L5
t = rotate_left(t)
return t # @line L6
def delete(t: Node[K] | None, k: K) -> Node[K] | None:
"""Delete k from the treap t; return the new subtree root.
The node of k is rotated down, always lifting the child of larger
priority, until it has at most one child. Cost O(depth of k + the
lengths of the two spines below it).
"""
if t is None: # @line L7
return None
if k < t.key: # @line L8
t.left = delete(t.left, k)
elif t.key < k: # @line L9
t.right = delete(t.right, k)
elif t.left is None: # @line L10
return t.right
elif t.right is None: # @line L11
return t.left
elif t.left.pri > t.right.pri: # @line L12
t = rotate_right(t)
t.right = delete(t.right, k)
else: # @line L13
t = rotate_left(t)
t.left = delete(t.left, k)
return t # @line L14
class Treap(Generic[K]):
"""An ordered set kept as a treap with random priorities.
Keys must be totally ordered. Search, insertion and deletion take
O(log n) expected time, over the random priorities, for every input.
"""
def __init__(self, seed: int | None = None) -> None:
self.root: Node[K] | None = None
self.size = 0
self._rng = random.Random(seed)
def contains(self, k: K) -> bool:
"""Return whether k is in the set. O(log n) expected."""
t = self.root
while t is not None:
if k < t.key:
t = t.left
elif t.key < k:
t = t.right
else:
return True
return False
def insert(self, k: K, priority: float | None = None) -> bool:
"""Add k; return False if it was present. A priority may be given
for tests; by default it is drawn uniformly from [0, 1)."""
if self.contains(k):
return False
p = self._rng.random() if priority is None else priority
self.root = insert(self.root, k, p)
self.size += 1
return True
def delete(self, k: K) -> bool:
"""Remove k; return False if it was absent. O(log n) expected."""
if not self.contains(k):
return False
self.root = delete(self.root, k)
self.size -= 1
return True
def keys(self) -> Iterator[K]:
"""Yield the keys in increasing order. O(n)."""
stack: list[Node[K]] = []
t = self.root
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 height(self) -> int:
"""The number of nodes on the longest downward path."""
best = 0
stack: list[tuple[Node[K], int]] = [(self.root, 1)] if self.root else []
while stack:
t, d = stack.pop()
best = max(best, d)
for c in (t.left, t.right):
if c is not None:
stack.append((c, d + 1))
return best
def __len__(self) -> int:
return self.size
def check_invariant(self) -> None:
"""Raise AssertionError unless keys are in search order and each
priority is at least the priorities of its children."""
count = 0
stack: list[tuple[Node[K], K | None, K | None]] = (
[(self.root, None, None)] if self.root else []
)
while stack:
t, lo, hi = stack.pop()
count += 1
assert lo is None or lo < t.key
assert hi is None or t.key < hi
for c in (t.left, t.right):
if c is not None:
assert c.pri <= t.pri, "heap order on priorities"
if t.left is not None:
stack.append((t.left, lo, t.key))
if t.right is not None:
stack.append((t.right, t.key, hi))
assert count == self.size
test_treap.py 128 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/treap/. The authors are listed in prov/S6DS4V.jsonl.
import itertools
import math
import random
import treap as tr
from treap import Node, Treap
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))
EXAMPLE = [(50, 30), (30, 60), (70, 40), (20, 10), (40, 80), (60, 50)]
def test_page_example() -> None:
t: Treap[int] = Treap()
want = {
30: (None, 30, 50),
70: (None, 30, (50, 70, None)),
40: ((20, 30, None), 40, (50, 70, None)),
60: ((20, 30, None), 40, (50, 60, 70)),
}
for k, p in EXAMPLE:
assert t.insert(k, p)
t.check_invariant()
if k in want:
assert shape(t.root) == want[k]
assert t.delete(40)
t.check_invariant()
assert shape(t.root) == (20, 30, (50, 60, 70))
assert t.delete(30)
t.check_invariant()
assert shape(t.root) == ((20, 50, None), 60, 70)
def test_shape_does_not_depend_on_order() -> None:
# the treap of a set of (key, priority) pairs is unique
shapes = set()
for perm in itertools.permutations(EXAMPLE):
t: Treap[int] = Treap()
for k, p in perm:
t.insert(k, p)
shapes.add(repr(shape(t.root)))
assert shapes == {repr(((20, 30, None), 40, (50, 60, 70)))}
def test_edge_cases() -> None:
t: Treap[int] = Treap(seed=0)
assert not t.contains(3)
assert not t.delete(3)
assert t.height() == 0
t.check_invariant()
assert t.insert(3)
assert not t.insert(3)
assert len(t) == 1
assert t.delete(3)
assert t.root is None
# equal priorities are allowed
for k in range(10):
t.insert(k, 0.5)
t.check_invariant()
assert list(t.keys()) == list(range(10))
def test_priorities_must_be_random() -> None:
# priority equal to the key turns the treap into a path
t: Treap[int] = Treap()
for k in range(200):
t.insert(k, float(k))
assert t.height() == 200
# random priorities on the same sorted input keep it short
u: Treap[int] = Treap(seed=1989)
for k in range(200):
u.insert(k)
assert u.height() <= 4 * math.log(200) + 10
def test_expected_rotations() -> None:
rng = random.Random(7)
count = [0]
left, right = tr.rotate_left, tr.rotate_right
def cl(x: "Node[int]") -> "Node[int]":
count[0] += 1
return left(x)
def cr(x: "Node[int]") -> "Node[int]":
count[0] += 1
return right(x)
tr.rotate_left, tr.rotate_right = cl, cr
try:
t: Treap[int] = Treap(seed=11)
keys = list(range(5000))
rng.shuffle(keys)
for k in keys:
t.insert(k)
# fewer than 2 rotations for each insertion on average
assert count[0] < 2 * len(keys)
finally:
tr.rotate_left, tr.rotate_right = left, right
def test_random_against_set() -> None:
rng = random.Random(1996)
for case in range(600):
t: Treap[int] = Treap(seed=case)
model: set[int] = set()
for _ in range(rng.randrange(1, 60)):
k = rng.randrange(40)
if rng.random() < 0.6:
assert t.insert(k) == (k not in model)
model.add(k)
else:
assert t.delete(k) == (k in model)
model.discard(k)
t.check_invariant()
assert list(t.keys()) == sorted(model)
for k in range(40):
assert t.contains(k) == (k in model)
What the next level needs
- the tests did not run in this build