AVL tree in Python
The first self-balancing BST: balance factors in {-1, 0, 1} are restored by single or double rotations after updates.
- File
avl_tree.py, 182 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/avl-tree/. The authors are listed in prov/195NAP.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", "left", "right", "h")
def __init__(self, key: K) -> None:
self.key = key
self.left: Node[K] | None = None
self.right: Node[K] | None = None
self.h = 1 # the number of nodes on the longest downward path
def height(t: Node[K] | None) -> int:
return t.h if t is not None else 0
def update(t: Node[K]) -> None:
t.h = 1 + max(height(t.left), height(t.right))
def bf(t: Node[K]) -> int:
"""Balance factor: height of the right subtree minus height of the left one."""
return height(t.right) - height(t.left)
def rotate_left(x: Node[K]) -> Node[K]:
y = x.right
assert y is not None
x.right = y.left
y.left = x
update(x)
update(y)
return y
def rotate_right(y: Node[K]) -> Node[K]:
x = y.left
assert x is not None
y.left = x.right
x.right = y
update(y)
update(x)
return x
def rebalance(t: Node[K]) -> Node[K]:
update(t) # @line L6
if bf(t) == 2: # @line L7
assert t.right is not None
if bf(t.right) < 0: # @line L8
t.right = rotate_right(t.right)
return rotate_left(t) # @line L9
if bf(t) == -2: # @line L10
assert t.left is not None
if bf(t.left) > 0: # @line L11
t.left = rotate_left(t.left)
return rotate_right(t) # @line L12
return t # @line L13
def insert(t: Node[K] | None, k: K) -> Node[K]:
if t is None: # @line L1
return Node(k)
if k < t.key: # @line L2
t.left = insert(t.left, k)
elif t.key < k: # @line L3
t.right = insert(t.right, k)
else:
return t # @line L4
return rebalance(t) # @line L5
def delete(t: Node[K] | None, k: K) -> Node[K] | None:
if t is None: # @line L14
return None
if k < t.key: # @line L15
t.left = delete(t.left, k)
elif t.key < k: # @line L16
t.right = delete(t.right, k)
else: # @line L17
if t.left is None: # @line L18
return t.right
if t.right is None: # @line L19
return t.left
m = t.right # @line L20
while m.left is not None:
m = m.left
t.key = m.key # @line L21
t.right = delete(t.right, m.key)
return rebalance(t) # @line L22
class AVLTree(Generic[K]):
"""An ordered set of keys kept in an AVL tree.
Each operation takes O(log n) time in the worst case for n keys, and the
recursion depth is the height, at most about 1.44 lg n. Keys must be
totally ordered by <; equal keys are stored once.
"""
def __init__(self) -> None:
self.root: Node[K] | None = None
self.n = 0
def __len__(self) -> int:
return self.n
def contains(self, k: K) -> bool:
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:
"""Insert k. Return True if k was not present."""
if self.contains(k):
return False
self.root = insert(self.root, k)
self.n += 1
return True
def delete(self, k: K) -> bool:
"""Delete k. Return True if k was present."""
if not self.contains(k):
return False
self.root = delete(self.root, k)
self.n -= 1
return True
def height(self) -> int:
return height(self.root)
def keys(self) -> list[K]:
out: list[K] = []
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()
out.append(t.key)
t = t.right
return out
def __iter__(self) -> Iterator[K]:
return iter(self.keys())
def check_invariant(self) -> None:
"""Raise AssertionError unless the tree is an AVL search tree of n keys."""
def walk(t: Node[K] | None, lo: K | None, hi: K | None) -> tuple[int, int]:
if t is None:
return 0, 0
assert lo is None or lo < t.key
assert hi is None or t.key < hi
hl, cl = walk(t.left, lo, t.key)
hr, cr = walk(t.right, t.key, hi)
assert t.h == 1 + max(hl, hr), "stored height is wrong"
assert abs(hr - hl) <= 1, "balance factor out of range"
return t.h, cl + cr + 1
_, count = walk(self.root, None, None)
assert count == self.n
test_avl_tree.py 88 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/avl-tree/. The authors are listed in prov/195NAP.jsonl.
import math
import random
from avl_tree import AVLTree, Node
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: AVLTree[int] = AVLTree()
for k in [10, 20, 30, 40, 50, 25]:
a.insert(k)
a.check_invariant()
assert shape(a.root) == ((10, 20, 25), 30, (None, 40, 50))
a.delete(40)
a.check_invariant()
assert shape(a.root) == ((10, 20, 25), 30, 50)
a.delete(50)
a.check_invariant()
assert shape(a.root) == (10, 20, (25, 30, None))
def test_sorted_input_is_perfect() -> None:
a: AVLTree[int] = AVLTree()
for k in range(1, 8):
a.insert(k)
assert shape(a.root) == ((1, 2, 3), 4, (5, 6, 7))
for k in range(8, 1024):
a.insert(k)
a.check_invariant()
assert a.height() == 10
def test_child_with_zero_balance_after_delete() -> None:
a: AVLTree[int] = AVLTree()
for k in [1, 7, 2, 6, 3, 5, 8, 4]:
a.insert(k)
assert shape(a.root) == ((1, 2, None), 3, ((4, 5, None), 6, (None, 7, 8)))
a.delete(1)
a.check_invariant()
assert shape(a.root) == ((2, 3, (4, 5, None)), 6, (None, 7, 8))
def test_edges() -> None:
a: AVLTree[int] = AVLTree()
assert not a.delete(1) and not a.contains(1) and a.height() == 0
assert a.insert(5) and not a.insert(5)
assert len(a) == 1 and a.keys() == [5]
assert a.delete(5) and len(a) == 0 and a.root is None
for k in [3, 3, 3]:
a.insert(k)
assert a.keys() == [3]
def test_height_bound() -> None:
rng = random.Random(4)
a: AVLTree[int] = AVLTree()
for _ in range(3000):
a.insert(rng.randint(0, 10**6))
n = len(a)
assert a.height() < 1.4405 * math.log2(n + 2) - 0.3277
def test_random_against_set() -> None:
rng = random.Random(1962)
for _ in range(500):
a: AVLTree[int] = AVLTree()
model: set[int] = set()
for _ in range(rng.randint(0, 80)):
k = rng.randint(0, 40)
if rng.random() < 0.6:
assert a.insert(k) == (k not in model)
model.add(k)
else:
assert a.delete(k) == (k in model)
model.discard(k)
assert a.contains(k) == (k in model)
a.check_invariant()
assert a.keys() == sorted(model)
What the next level needs
- the tests did not run in this build