DSA AVL Trees
An AVL tree is a self-balancing binary search tree that keeps every node's left and right subtree heights within one of each other, guaranteeing O(log n) operations even in the worst case.
Why Self-Balancing Trees?
A plain binary search tree only stays fast if it stays balanced. Insert already-sorted data and it degenerates into a straight chain of nodes, turning O(log n) search into O(n). Named after inventors Adelson-Velsky and Landis, the AVL tree fixes this by enforcing a balance condition after every insertion or deletion: for every node, the height of its left subtree and the height of its right subtree may differ by at most 1.
That difference - left height minus right height - is called the balance factor, and it must always be -1, 0, or 1. Whenever an insertion or deletion pushes a node's balance factor outside that range, the tree performs a rotation: a local restructuring of a few nodes that restores balance without disturbing the BST ordering invariant. Because each rotation is O(1) and at most O(log n) of them are needed per operation, an AVL tree keeps search, insert, and delete at O(log n) even in the worst case.
Balance Factor and the Four Rotation Cases
Example: Balance Factor and a Single Rotation
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
self.height = 1 # height of a leaf is 1
def height(node):
return node.height if node else 0
def balance_factor(node):
return height(node.left) - height(node.right)
def update_height(node):
node.height = 1 + max(height(node.left), height(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 # x is the 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 y # y is the new subtree root
# Manually build a left-left heavy case: 30 with left child 20,
# which itself has left child 10.
root = Node(30)
root.left = Node(20)
root.left.left = Node(10)
update_height(root.left)
update_height(root)
print(balance_factor(root)) # 2 - unbalanced
root = rotate_right(root) # fix it with a single right rotation
print(root.value, root.left.value, root.right.value) # 20 10 30
print(balance_factor(root)) # 0 - balanced againInserting Into an AVL Tree
Insertion starts exactly like a normal BST insert: walk down comparing values until you find the right spot for the new node. The difference is what happens on the way back up the recursion - at each ancestor, recompute its height, compute its balance factor, and if it's out of range, apply one of four rotation patterns (left-left, right-right, left-right, or right-left) based on which side is heavy and which side of that side is heavy.
Example: Full AVL Insert With Rotations
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
self.height = 1
def height(node):
return node.height if node else 0
def balance_factor(node):
return height(node.left) - height(node.right)
def update_height(node):
node.height = 1 + max(height(node.left), height(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
def rotate_left(x):
y = x.right
t2 = y.left
y.left = x
x.right = t2
update_height(x)
update_height(y)
return y
def insert(node, value):
if node is None:
return Node(value)
if value < node.value:
node.left = insert(node.left, value)
elif value > node.value:
node.right = insert(node.right, value)
else:
return node # no duplicates
update_height(node)
balance = balance_factor(node)
if balance > 1 and value < node.left.value: # Left-Left
return rotate_right(node)
if balance < -1 and value > node.right.value: # Right-Right
return rotate_left(node)
if balance > 1 and value > node.left.value: # Left-Right
node.left = rotate_left(node.left)
return rotate_right(node)
if balance < -1 and value < node.right.value: # Right-Left
node.right = rotate_right(node.right)
return rotate_left(node)
return node
def inorder(node, result=None):
if result is None:
result = []
if node is None:
return result
inorder(node.left, result)
result.append(node.value)
inorder(node.right, result)
return result
root = None
for value in [10, 20, 30, 40, 50, 25]:
root = insert(root, value)
print(inorder(root)) # [10, 20, 25, 30, 40, 50] - still sorted
print(root.value) # 30 - the tree re-balanced around a new root
print(height(root)) # 3 - not 6, even though 6 values were insertedExample: AVL Stays Balanced on Sorted Input
class Node:
def __init__(self, value):
self.value = value
self.left = None
self.right = None
self.height = 1
def height(node):
return node.height if node else 0
def balance_factor(node):
return height(node.left) - height(node.right)
def update_height(node):
node.height = 1 + max(height(node.left), height(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
def rotate_left(x):
y = x.right
t2 = y.left
y.left = x
x.right = t2
update_height(x)
update_height(y)
return y
def insert(node, value):
if node is None:
return Node(value)
if value < node.value:
node.left = insert(node.left, value)
elif value > node.value:
node.right = insert(node.right, value)
else:
return node
update_height(node)
balance = balance_factor(node)
if balance > 1 and value < node.left.value:
return rotate_right(node)
if balance < -1 and value > node.right.value:
return rotate_left(node)
if balance > 1 and value > node.left.value:
node.left = rotate_left(node.left)
return rotate_right(node)
if balance < -1 and value < node.right.value:
node.right = rotate_right(node.right)
return rotate_left(node)
return node
# Insert already-sorted data: 1, 2, 3, ..., 15
root = None
for value in range(1, 16):
root = insert(root, value)
print(height(root)) # 4 - stays logarithmic (log2(15) is about 3.9)
print(balance_factor(root)) # always within -1, 0, or 1
# A plain (non-balancing) BST inserting the same sorted data would instead
# form a single 15-node chain with a height of 15, making every search O(n).- Balance factor equals height(left subtree) minus height(right subtree), and must stay in -1, 0, or 1
- A single rotation fixes a left-left or right-right heavy case
- A double rotation fixes a left-right or right-left heavy case
- Tree height stays O(log n) no matter what order values are inserted in
- Search, insert, and delete are all guaranteed O(log n), even in the worst case
Exercise: DSA AVL Trees
What defining property must every node in an AVL tree satisfy?