| name | algo-avl-trees |
| description | Implement a self-balancing AVL binary search tree in Python with rotation-based rebalancing (LL/RR/LR/RL) guaranteeing O(log n) insert/delete/search. Use when a user asks to build/implement an AVL tree, keep a sorted index balanced under insert/delete, explain balance factor or tree rotations, or avoid O(n) degeneration of a BST on sorted/near-sorted input (e.g. genomic positions arriving in coordinate order). |
| tool_type | python |
| primary_tool | Python |
AVL Trees
When to Use
- Implementing or explaining a self-balancing binary search tree from scratch (interview prep, coursework, "code up an AVL tree").
- Maintaining a sorted index that must stay O(log n) for search/insert/delete even when input arrives sorted or near-sorted (e.g. variant calls streaming in by chromosomal position, timestamps, incrementally arriving sequencing reads).
- Debugging a BST that "degenerates to a linked list" because inputs come pre-sorted.
- Comparing AVL vs. plain BST vs. red-black tree height/performance trade-offs.
- Walking through rotation cases (LL/RR/LR/RL) or balance-factor bookkeeping for a homework/exam question.
Version Compatibility
Pure-Python, stdlib only — no version dependencies. Works unchanged on Python ≥ 3.8 (uses only built-in types; type hints use Optional/List from typing, available since 3.5+).
Prerequisites
- Comfortable with recursive binary search tree insert/delete/search (see
algo-binary-search-trees).
- No external packages required (
pip install nothing).
- Helpful background: Big-O complexity analysis (
algo-complexity-analysis).
Key Invariant
Balance factor bf(node) = height(left) - height(right). Every node must satisfy |bf| <= 1.
This bounds height at h < 1.44 * log2(n+2), guaranteeing O(log n) operations even on adversarial (sorted) input — where a plain BST would degrade to O(n).
Complexity
| Operation | Time | Space |
|---|
| Search / Insert / Delete | O(log n) | O(log n) recursion stack |
| Single rotation | O(1) | O(1) |
| Get height / balance | O(1) | O(1) |
Rotation Decision Table
| Node bf | Child bf | Case | Fix |
|---|
| +2 | >= 0 | LL | Right rotation |
| +2 | < 0 | LR | Left rotation on left child, then right rotation |
| -2 | <= 0 | RR | Left rotation |
| -2 | > 0 | RL | Right rotation on right child, then left rotation |
Insertion needs at most 1 rotation (the tree is balanced from the point of insertion up). Deletion may need O(log n) rotations, one at every ancestor up to the root.
Goal: implement a fully working AVL tree supporting insert, delete, search, and sorted traversal, so a BST stays O(log n) even on sorted input.
Approach: store height on each node (not balance factor directly); after every recursive insert/delete, refresh the ancestor's height, compute its balance factor, and apply the matching rotation(s) from the table above.
from typing import Optional, List, Any
class AVLNode:
"""A node in an AVL tree.
Attributes:
value: The data stored in the node.
left: Reference to left child.
right: Reference to right child.
height: Height of the subtree rooted here (leaf = 1).
"""
def __init__(self, value: Any) -> None:
self.value = value
self.left: Optional["AVLNode"] = None
self.right: Optional["AVLNode"] = None
self.height: int = 1
class AVLTree:
"""Self-balancing BST maintaining |balance factor| <= 1 at every node."""
def __init__(self) -> None:
self.root: Optional[AVLNode] = None
def get_height(self, node: Optional[AVLNode]) -> int:
"""Height of a node; an empty (None) node has height 0."""
return node.height if node else 0
def () -> :
.get_height(node.left) - .get_height(node.right) node
() -> :
node.height = + (.get_height(node.left), .get_height(node.right))
() -> AVLNode:
y, T3 = z.left, z.left.right
y.right, z.left = z, T3
._update_height(z)
._update_height(y)
y
() -> AVLNode:
y, T2 = z.right, z.right.left
y.left, z.right = z, T2
._update_height(z)
._update_height(y)
y
() -> :
.root = ._insert(.root, value)
() -> AVLNode:
node :
AVLNode(value)
value < node.value:
node.left = ._insert(node.left, value)
value > node.value:
node.right = ._insert(node.right, value)
:
node
._update_height(node)
balance = .get_balance(node)
balance > value < node.left.value:
.rotate_right(node)
balance < - value > node.right.value:
.rotate_left(node)
balance > value > node.left.value:
node.left = .rotate_left(node.left)
.rotate_right(node)
balance < - value < node.right.value:
node.right = .rotate_right(node.right)
.rotate_left(node)
node
() -> :
.root = ._delete(.root, value)
() -> AVLNode:
current = node
current.left :
current = current.left
current
() -> [AVLNode]:
node :
value < node.value:
node.left = ._delete(node.left, value)
value > node.value:
node.right = ._delete(node.right, value)
:
node.left :
node.right
node.right :
node.left
successor = ._get_min_node(node.right)
node.value = successor.value
node.right = ._delete(node.right, successor.value)
._update_height(node)
balance = .get_balance(node)
balance > .get_balance(node.left) >= :
.rotate_right(node)
balance > .get_balance(node.left) < :
node.left = .rotate_left(node.left)
.rotate_right(node)
balance < - .get_balance(node.right) <= :
.rotate_left(node)
balance < - .get_balance(node.right) > :
node.right = .rotate_right(node.right)
.rotate_left(node)
node
() -> [AVLNode]:
node = .root
node :
value == node.value:
node
node = node.left value < node.value node.right
() -> []:
result: [] = []
._inorder(.root, result)
result
() -> :
node:
._inorder(node.left, result)
result.append(node.value)
._inorder(node.right, result)
Goal: show why AVL matters — quantify how much a plain BST degrades on sorted input (e.g. variants sorted by genomic position) versus the AVL guarantee.
Approach: insert the same sorted sequence into a naive BST and an AVL tree and compare resulting heights.
import math
class _PlainBSTNode:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
def build_plain_bst(values):
"""Insert values into an unbalanced BST (no rotations) and return the root."""
root = None
for v in values:
if root is None:
root = _PlainBSTNode(v)
continue
node = root
while True:
if v < node.value:
if node.left is None:
node.left = _PlainBSTNode(v)
break
node = node.left
else:
if node.right is None:
node.right = _PlainBSTNode(v)
break
node = node.right
return root
def plain_bst_height(node) -> int:
"""Height of a plain (unbalanced) BST subtree; None has height 0."""
if node is None:
return 0
return + (plain_bst_height(node.left), plain_bst_height(node.right))
() -> :
sorted_vals = ((, n + ))
bst_root = build_plain_bst(sorted_vals)
avl = AVLTree()
v sorted_vals:
avl.insert(v)
optimal = math.ceil(math.log2(n + ))
(
)
__name__ == :
n (, , ):
compare_heights(n)
Pitfalls
- Update height before checking balance: height must be refreshed on the way back up the recursion, before computing
bf.
- Deletion may cascade: unlike insertion (at most one rotation), deletion can require rotations at every ancestor up to the root — don't
return early after the first fix.
- Child bf == 0 on deletion: when a node has
bf=+2 and its left child has bf=0, that is still an LL case (right rotation), not LR. This differs from insertion, where bf=0 never triggers a rotation on the child.
- Height of None is 0, height of a leaf is 1: mixing up these conventions causes off-by-one errors in balance-factor calculation.
- Don't store balance factor directly: store height and compute
bf on the fly; storing bf directly requires extra bookkeeping during rotations and is easy to get wrong.
- No duplicate handling built in: the reference implementation silently ignores inserts of a value already present; decide explicitly whether you need multiset semantics (e.g. store a count per node) before reusing this code.
See Also
algo-binary-search-trees — the unbalanced BST this structure improves on.
algo-red-black-trees — an alternative self-balancing BST with looser balance (fewer rotations on write-heavy workloads).
algo-suffix-trees — a different balanced-tree use case (substring indexing) with unrelated rebalancing rules.
algo-complexity-analysis — background for the O(log n) height bound derivation.