Python Tutorial
AVL Trees
A self-balancing binary search tree that rotates after each change to guarantee O(log n) operations.
Why Balance Matters
A plain BST can degenerate into a linked list, making operations O(n). An AVL tree (named after Adelson-Velsky and Landis) keeps itself balanced so height stays O(log n). After every insert or delete it checks each node's balance factor and rotates when needed.
balance factor = height(left subtree) − height(right subtree)
A node is balanced when its balance factor is −1, 0, or +1. Any other value triggers a rotation.
The Four Rotation Cases
| Case | Condition | Fix |
|---|---|---|
| Left-Left | balance > 1 and key < left.key | Right rotation |
| Right-Right | balance < −1 and key > right.key | Left rotation |
| Left-Right | balance > 1 and key > left.key | Left then Right |
| Right-Left | balance < −1 and key < right.key | Right then Left |
Node and Rotations
class Node:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.height = 1
def h(node):
return node.height if node else 0
def balance(node):
return h(node.left) - h(node.right) if node else 0
def update_height(node):
node.height = 1 + max(h(node.left), h(node.right))
def rotate_right(y):
x = y.left
t2 = x.right
x.right = y
y.left = t2
update_height(y)
update_height(x)
return x # new subtree root
def rotate_left(x):
y = x.right
t2 = y.left
y.left = x
x.right = t2
update_height(x)
update_height(y)
return yBalanced Insertion
def insert(node, key):
if node is None:
return Node(key)
if key < node.key:
node.left = insert(node.left, key)
elif key > node.key:
node.right = insert(node.right, key)
else:
return node
update_height(node)
b = balance(node)
if b > 1 and key < node.left.key: # Left-Left
return rotate_right(node)
if b < -1 and key > node.right.key: # Right-Right
return rotate_left(node)
if b > 1 and key > node.left.key: # Left-Right
node.left = rotate_left(node.left)
return rotate_right(node)
if b < -1 and key < node.right.key: # Right-Left
node.right = rotate_right(node.right)
return rotate_left(node)
return node
root = None
for k in [10, 20, 30, 40, 50, 25]: # sorted-ish input stays balanced
root = insert(root, k)
print(root.key) # 30 -> tree auto-balanced, not a chainInsert 10, 20, 30 into a plain BST and you get a right-leaning chain; the AVL tree rotates so 20 becomes the root, keeping height minimal.
AVL vs Red-Black Trees
- AVL: more strictly balanced → faster lookups, but more rotations on insert/delete.
- Red-Black: looser balance → fewer rotations, favored for write-heavy workloads (used in many language libraries).
Best Practices
- Use a balanced tree when you need guaranteed O(log n) ordered operations.
- Update heights bottom-up after each recursive call before checking balance.
- In real Python code, prefer
sortedcontainersor a dict/heap unless you specifically need an ordered tree. - AVL shines for lookup-heavy data; Red-Black for insert/delete-heavy data.
Try It Yourself
Exercise 1: A node has left-subtree height 3 and right-subtree height 1. What is its balance factor, and is it balanced?
Show solution
Balance factor = 3 − 1 = 2. Since it is outside −1..+1, the node is unbalanced and needs a rotation.
Exercise 2: When would you choose a Red-Black tree over an AVL tree?
Show solution
For write-heavy workloads — Red-Black trees do fewer rotations on insert/delete, at the cost of slightly slower lookups.
📘 Real-World Deep Dive
Knowing <strong>DSA Avl Trees (algorithms & data structures)</strong> well is what turns algorithms & data structures from a curiosity into a daily tool — you'll reach for it in nearly every real project.
Real-Life Scenario
An end-to-end usage of DSA Avl Trees that you'd actually see in a data pipeline or analytics notebook.
Real-Life Example
class Node:
def __init__(self, k):
self.k, self.h, self.l, self.r = k, 1, None, None
def height(n): return n.h if n else 0
def update(n): n.h = 1 + max(height(n.l), height(n.r))
def rotate_right(y):
x, t2 = y.l, y.l.r
x.r, y.l = y, t2
update(y); update(x); return x
def rotate_left(x):
y, t2 = x.r, x.r.l
y.l, x.r = x, t2
update(x); update(y); return y
def rebalance(n):
update(n)
if height(n.l) - height(n.r) > 1:
return rotate_right(n) if height(n.l.l) >= height(n.l.r) else rotate_right(Node(n.k)); # simplified
if height(n.r) - height(n.l) > 1:
return rotate_left(n) if height(n.r.r) >= height(n.r.l) else rotate_left(Node(n.k))
return n
# Demonstration stub: insert three keys and rebalance.
root = None
for k in [10, 20, 30]:
root = Node(k) if root is None else (rotate_left(root) if k > root.k else root)
update(root)
print("root after inserts:", root.k, "height:", root.h)Expected Output
(see source)Common mistakes
- Off-by-one errors in binary-search: the standard idiom is
while lo <= hiwithmid = (lo + hi) // 2. - Recursive algorithms blow the stack for n > ~10⁴; convert to iterative with an explicit stack.
- Comparison algorithms (
sorted(iterable)) are stable by default in Python — surprising for Java/C++ users. - Treating DSA Avl Trees as a black box without reading the docs — the API has subtle defaults that bite when you scale.
🚀 Performance & Best Practices
- Use
bisect.bisect_left/bisect_rightinstead of writing your own binary search. - Convert a sorted search into a tuple-access pattern with
numpy.searchsortedfor huge arrays. - Use
heapqfor priority queues instead of maintaining a sorted list manually. - When working with algorithms & data structures, prefer vectorised / batched operations over Python loops.
🧪 Try It Yourself
- Reproduce the snippet on a representative slice of your own data.
- Profile the snippet with
cProfileortimeitand find the single biggest improvement. - Generalise the snippet into a small, reusable function you can drop into future projects.