AA tree in Python
A simpler red-black variant where only right-leaning red links are allowed, using a level field instead of colours.
- File
aa_tree.py, 200 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/aa-tree/. The authors are listed in prov/TPBFGF.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]):
"""A node with its level; nil has level 0."""
__slots__ = ("key", "left", "right", "level")
def __init__(self, key: K) -> None:
self.key = key
self.left: Node[K] | None = None
self.right: Node[K] | None = None
self.level = 1
def level(t: Node[K] | None) -> int:
return 0 if t is None else t.level
def skew(t: Node[K] | None) -> Node[K] | None:
"""Remove a horizontal left link at t by a right rotation. O(1)."""
if t is not None and t.left is not None and t.left.level == t.level: # @line L1
l = t.left # @line L2
t.left = l.right
l.right = t
return l
return t
def split(t: Node[K] | None) -> Node[K] | None:
"""Remove two horizontal right links in a row at t by a left rotation
that lifts the middle node one level. O(1)."""
if (
t is not None
and t.right is not None
and t.right.right is not None
and t.right.right.level == t.level # @line L3
):
r = t.right # @line L4
t.right = r.left
r.left = t
r.level += 1 # @line L5
return r
return t
def insert(t: Node[K] | None, k: K) -> Node[K]:
"""Insert k below t (k must be absent) and return the new root. O(log n)."""
if t is None: # @line L6
return Node(k)
if k < t.key: # @line L7
t.left = insert(t.left, k)
else:
t.right = insert(t.right, k)
t = skew(t) # @line L8
t = split(t)
assert t is not None
return t
def decrease_level(t: Node[K]) -> None:
"""Lower t (and a horizontal right child) to one above its lower child."""
should = min(level(t.left), level(t.right)) + 1 # @line L16
if should < t.level: # @line L17
t.level = should
if t.right is not None and should < t.right.level:
t.right.level = should
def delete(t: Node[K] | None, k: K) -> Node[K] | None:
"""Delete k from below t (k must be present) and return the new root.
O(log n)."""
assert t is not None
if k < t.key: # @line L9
t.left = delete(t.left, k)
elif t.key < k:
t.right = delete(t.right, k)
elif t.left is None and t.right is None: # @line L10
return None
elif t.left is None: # @line L11
assert t.right is not None
s = t.right
while s.left is not None:
s = s.left
t.key = s.key
t.right = delete(t.right, s.key)
else: # @line L12
p = t.left
while p.right is not None:
p = p.right
t.key = p.key
t.left = delete(t.left, p.key)
decrease_level(t) # @line L13
t = skew(t) # @line L14
assert t is not None
t.right = skew(t.right)
if t.right is not None:
t.right.right = skew(t.right.right)
t = split(t) # @line L15
assert t is not None
t.right = split(t.right)
return t
class AATree(Generic[K]):
"""An ordered set kept as an AA tree (Andersson 1993).
Keys must be totally ordered. Search, insertion and deletion cost
O(log n) time in the worst case; the height is at most 2 lg(n + 1).
"""
def __init__(self) -> None:
self.root: Node[K] | None = None
self.size = 0
def contains(self, k: K) -> bool:
"""Return whether k is in the set. O(log n)."""
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) -> bool:
"""Add k; return False if it was already present. O(log n)."""
if self.contains(k):
return False
self.root = insert(self.root, k)
self.size += 1
return True
def delete(self, k: K) -> bool:
"""Remove k; return False if it was absent. O(log n)."""
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:
def ht(t: Node[K] | None) -> int:
return 0 if t is None else 1 + max(ht(t.left), ht(t.right))
return ht(self.root)
def __len__(self) -> int:
return self.size
def check_invariant(self) -> None:
"""Raise AssertionError unless the tree is a valid AA tree."""
count = 0
def walk(t: Node[K] | None, lo: K | None, hi: K | None) -> None:
nonlocal count
if t is None:
return
count += 1
assert lo is None or lo < t.key
assert hi is None or t.key < hi
if t.left is None and t.right is None:
assert t.level == 1, "(A1) a leaf has level 1"
assert level(t.left) == t.level - 1, "(A2) left child one level down"
assert level(t.right) in (t.level, t.level - 1), "(A3) right child"
if t.right is not None:
assert level(t.right.right) < t.level, "(A4) no two horizontal links"
if t.level > 1:
assert t.left is not None and t.right is not None, "(A5)"
walk(t.left, lo, t.key)
walk(t.right, t.key, hi)
walk(self.root, None, None)
assert count == self.size
test_aa_tree.py 107 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/aa-tree/. The authors are listed in prov/TPBFGF.jsonl.
import math
import random
from aa_tree import AATree, Node, skew, split
def shape(t: "Node[int] | None") -> object:
if t is None:
return None
if t.left is None and t.right is None:
return f"{t.key}:{t.level}"
return (shape(t.left), f"{t.key}:{t.level}", shape(t.right))
def test_page_example() -> None:
t: AATree[int] = AATree()
steps = {
2: (None, "1:1", "2:1"),
3: ("1:1", "2:2", "3:1"),
5: ("1:1", "2:2", ("3:1", "4:2", "5:1")),
7: (("1:1", "2:2", "3:1"), "4:3", ("5:1", "6:2", "7:1")),
}
for k in range(1, 8):
assert t.insert(k)
t.check_invariant()
if k in steps:
assert shape(t.root) == steps[k]
assert t.delete(1)
t.check_invariant()
assert shape(t.root) == ((None, "2:1", "3:1"), "4:2", ("5:1", "6:2", "7:1"))
assert t.delete(4)
t.check_invariant()
assert shape(t.root) == ("2:1", "3:2", ("5:1", "6:2", "7:1"))
assert list(t.keys()) == [2, 3, 5, 6, 7]
def test_skew_and_split() -> None:
a, b = Node(1), Node(2)
b.left = a # horizontal left link: 1 and 2 both at level 1
r = skew(b)
assert r is a and a.right is b and b.left is None
c = Node(3)
b.right = c # now 1 -> 2 -> 3 all at level 1
r2 = split(a)
assert r2 is b and b.level == 2 and b.left is a and b.right is c
def test_edge_cases() -> None:
t: AATree[int] = AATree()
assert not t.contains(1)
assert not t.delete(1)
t.check_invariant()
assert t.insert(1)
assert not t.insert(1)
assert len(t) == 1
assert t.delete(1)
assert t.root is None
t.check_invariant()
def test_delete_needs_all_three_skews() -> None:
# Pitfall: the deletion fix-up must skew t, t.right and t.right.right.
# Deleting 7 lowers the root and its right child 18 to level 1; then
# 13 and 15 are horizontal left links at two places on the right spine.
t: AATree[int] = AATree()
for k in [18, 15, 19, 7, 3, 13]:
t.insert(k)
assert shape(t.root) == ("3:1", "7:2", ((None, "13:1", "15:1"), "18:2", "19:1"))
assert t.delete(7)
t.check_invariant()
assert shape(t.root) == ("3:1", "13:2", ("15:1", "18:2", "19:1"))
def test_sorted_input_height() -> None:
t: AATree[int] = AATree()
n = 2000
for k in range(n):
t.insert(k)
t.check_invariant()
assert t.height() <= 2 * math.log2(n + 1)
for k in range(n - 1, -1, -2):
assert t.delete(k)
t.check_invariant()
assert list(t.keys()) == list(range(0, n, 2))
def test_random_against_set() -> None:
rng = random.Random(1993)
for _ in range(600):
t: AATree[int] = AATree()
s: set[int] = set()
for _ in range(rng.randrange(1, 80)):
k = rng.randrange(60)
if rng.random() < 0.55:
assert t.insert(k) == (k not in s)
s.add(k)
else:
assert t.delete(k) == (k in s)
s.discard(k)
t.check_invariant()
assert t.contains(k) == (k in s)
assert list(t.keys()) == sorted(s)
if s:
assert t.height() <= 2 * math.log2(len(s) + 1)
What the next level needs
- the tests did not run in this build