Implicit treap in Python
A treap keyed by position in the sequence rather than by stored key, supporting insertion, deletion, reversal and range operations on arrays.
- File
implicit_treap.py, 184 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/implicit-treap/. The authors are listed in prov/47AZT5.jsonl.
from __future__ import annotations
import random
from typing import Iterator
class Node:
"""A treap node keyed by position. size and total describe the subtree;
rev means that the subtree must still be mirrored (a lazy reversal)."""
__slots__ = ("value", "prio", "left", "right", "size", "total", "rev")
def __init__(self, value: int, prio: float) -> None:
self.value = value
self.prio = prio
self.left: Node | None = None
self.right: Node | None = None
self.size = 1
self.total = value
self.rev = False
def size(t: Node | None) -> int:
return 0 if t is None else t.size
def total(t: Node | None) -> int:
return 0 if t is None else t.total
def push(t: Node) -> None:
"""Apply a pending reversal at t and pass it to the children. O(1)."""
if t.rev:
t.left, t.right = t.right, t.left
for c in (t.left, t.right):
if c is not None:
c.rev = not c.rev
t.rev = False
def update(t: Node) -> None:
"""Recompute size and total of t from its children. O(1)."""
t.size = size(t.left) + size(t.right) + 1
t.total = total(t.left) + total(t.right) + t.value
def split(t: Node | None, i: int) -> tuple[Node | None, Node | None]:
"""Split t into its first i items and the rest. O(depth)."""
if t is None: # @line L1
return None, None
push(t) # @line L2
if size(t.left) < i: # @line L3
a, b = split(t.right, i - size(t.left) - 1)
t.right = a
update(t)
return t, b
a, b = split(t.left, i) # @line L4
t.left = b
update(t)
return a, t
def merge(a: Node | None, b: Node | None) -> Node | None:
"""Concatenate a and b (all of a comes first). O(depth)."""
if a is None or b is None: # @line L5
return a if b is None else b
if a.prio > b.prio: # @line L6
push(a)
a.right = merge(a.right, b)
update(a)
return a
push(b) # @line L7
b.left = merge(a, b.left)
update(b)
return b
class ImplicitTreap:
"""A list of integers kept as an implicit treap.
Positions start at 0. insert, delete, get, reverse and range_sum take
O(log n) expected time; the priorities come from rng (or are given).
"""
def __init__(self, rng: random.Random | None = None) -> None:
self.root: Node | None = None
self.rng = rng if rng is not None else random.Random()
def __len__(self) -> int:
return size(self.root)
def insert(self, i: int, x: int, prio: float | None = None) -> None:
"""Insert x before position i (0 <= i <= n)."""
if not 0 <= i <= len(self):
raise IndexError(i)
p = self.rng.random() if prio is None else prio
a, b = split(self.root, i) # @line L8
self.root = merge(merge(a, Node(x, p)), b)
def append(self, x: int, prio: float | None = None) -> None:
self.insert(len(self), x, prio)
def delete(self, i: int) -> int:
"""Remove and return the item at position i."""
if not 0 <= i < len(self):
raise IndexError(i)
a, b = split(self.root, i) # @line L9
m, c = split(b, 1)
assert m is not None
self.root = merge(a, c)
return m.value
def reverse(self, lo: int, hi: int) -> None:
"""Reverse the items at positions lo .. hi - 1."""
if not 0 <= lo <= hi <= len(self):
raise IndexError((lo, hi))
a, b = split(self.root, lo) # @line L10
m, c = split(b, hi - lo)
if m is not None:
m.rev = not m.rev # @line L11
self.root = merge(a, merge(m, c))
def range_sum(self, lo: int, hi: int) -> int:
"""Return the sum of the items at positions lo .. hi - 1."""
if not 0 <= lo <= hi <= len(self):
raise IndexError((lo, hi))
a, b = split(self.root, lo) # @line L12
m, c = split(b, hi - lo)
s = total(m)
self.root = merge(a, merge(m, c))
return s
def get(self, i: int) -> int:
"""Return the item at position i, walking down one path."""
if not 0 <= i < len(self):
raise IndexError(i)
t = self.root
while t is not None:
push(t)
s = size(t.left)
if i < s:
t = t.left
elif i > s:
i -= s + 1
t = t.right
else:
return t.value
raise AssertionError("unreachable")
def __iter__(self) -> Iterator[int]:
stack: list[Node] = []
t = self.root
while stack or t is not None:
while t is not None:
push(t)
stack.append(t)
t = t.left
t = stack.pop()
yield t.value
t = t.right
def to_list(self) -> list[int]:
return list(self)
def height(self) -> int:
def h(t: Node | None) -> int:
return -1 if t is None else 1 + max(h(t.left), h(t.right))
return h(self.root)
def check_invariant(self) -> None:
"""Assert the heap order on priorities and the sizes and totals."""
def go(t: Node | None) -> None:
if t is None:
return
for c in (t.left, t.right):
assert c is None or c.prio <= t.prio, "heap order"
assert t.size == size(t.left) + size(t.right) + 1, "size"
assert t.total == total(t.left) + total(t.right) + t.value, "total"
go(t.left)
go(t.right)
go(self.root)
test_implicit_treap.py 111 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/implicit-treap/. The authors are listed in prov/47AZT5.jsonl.
import math
import random
import pytest
from implicit_treap import ImplicitTreap, merge, push, split
def shape(t):
if t is None:
return None
push(t)
if t.left is None and t.right is None:
return t.value
return (shape(t.left), t.value, shape(t.right))
def page_treap():
t = ImplicitTreap()
for v, p in zip([10, 20, 30, 40, 50], [3, 5, 1, 4, 2]):
t.append(v, p)
return t
def test_page_example():
t = page_treap()
assert shape(t.root) == (10, 20, (30, 40, 50))
a, b = split(t.root, 1)
assert shape(a) == 10 and shape(b) == (None, 20, (30, 40, 50))
m, c = split(b, 3)
assert shape(m) == (None, 20, (30, 40, None)) and shape(c) == 50
t.root = merge(a, merge(m, c))
t.reverse(1, 4)
assert t.root.value == 20 and not t.root.rev # the flag was pushed by merge
assert t.to_list() == [10, 40, 30, 20, 50]
assert shape(t.root) == ((10, 40, 30), 20, 50)
t.insert(2, 25, 6)
assert shape(t.root) == ((10, 40, None), 25, (30, 20, 50))
assert t.range_sum(1, 4) == 95
assert t.delete(0) == 10 and t.to_list() == [40, 25, 30, 20, 50]
t.check_invariant()
def test_edge_cases():
t = ImplicitTreap(random.Random(3))
assert len(t) == 0 and t.to_list() == [] and t.range_sum(0, 0) == 0
t.reverse(0, 0)
with pytest.raises(IndexError):
t.get(0)
with pytest.raises(IndexError):
t.insert(1, 5)
t.insert(0, 5)
t.reverse(0, 1)
assert t.get(0) == 5 and t.delete(0) == 5 and t.root is None
assert split(None, 3) == (None, None) and merge(None, None) is None
def test_double_reverse_cancels():
t = ImplicitTreap(random.Random(8))
for v in range(100):
t.append(v)
t.reverse(10, 90)
t.reverse(10, 90)
assert t.to_list() == list(range(100))
t.reverse(0, 100)
assert t.to_list() == list(range(99, -1, -1))
t.check_invariant()
def test_expected_height():
t = ImplicitTreap(random.Random(1997))
n = 20000
for v in range(n):
t.append(v)
assert t.height() <= 5 * math.log(n)
assert [t.get(i) for i in range(0, n, 997)] == list(range(0, n, 997))
def test_random_against_list():
rng = random.Random(1989)
for case in range(600):
t = ImplicitTreap(random.Random(case))
ref: list[int] = []
for _ in range(rng.randint(0, 60)):
op = rng.random()
n = len(ref)
if op < 0.35 or n == 0:
i, x = rng.randint(0, n), rng.randint(-50, 50)
t.insert(i, x)
ref.insert(i, x)
elif op < 0.5:
i = rng.randrange(n)
assert t.delete(i) == ref.pop(i)
elif op < 0.7:
lo = rng.randint(0, n)
hi = rng.randint(lo, n)
t.reverse(lo, hi)
ref[lo:hi] = ref[lo:hi][::-1]
elif op < 0.85:
lo = rng.randint(0, n)
hi = rng.randint(lo, n)
assert t.range_sum(lo, hi) == sum(ref[lo:hi])
else:
i = rng.randrange(n)
assert t.get(i) == ref[i]
assert len(t) == len(ref)
t.check_invariant()
assert t.to_list() == ref
What the next level needs
- the tests did not run in this build