Skip to content

Binary Search Trees and Balanced Trees

A filing cabinet that stays sorted: A BST is like a filing cabinet where every folder is labelled, and you know that smaller labels go left and larger labels go right — finding any file takes O(log n) time if the tree is balanced, just like binary search on a sorted array.

Why it matters: BSTs are the foundation of many data structures — balanced BSTs (AVL, red-black) power database indexes, language libraries (C++ std::map, Java TreeMap), and filesystem directories.

The key insight: An unbalanced BST degenerates into a linked list with O(n) operations — this is why self-balancing variants (AVL, red-black) are essential in practice. The balance property guarantees O(log n) height.

A binary search tree (BST) is a binary tree where every node satisfies the BST property: for any Node with key kkAll keys in its left subtree are strictly less than kkAnd all keys in its Right subtree are strictly greater than kk.

class BSTNode:
def __init__(self, key, val=None):
self.key = key
self.val = val
self.left = None
self.right = None
self.parent = None
def bst_search(root, key):
"""
Search for key in BST.
Time: O(h) where h = tree height
Best case (balanced): O(log n)
Worst case (degenerate): O(n)
"""
curr = root
while curr:
if key == curr.key:
return curr
elif key < curr.key:
curr = curr.left
else:
curr = curr.right
return None
def bst_insert(root, key, val=None):
"""
Insert key into BST. Returns new root.
Time: O(h)
"""
if root is None:
return BSTNode(key, val)
if key < root.key:
root.left = bst_insert(root.left, key, val)
root.left.parent = root
elif key > root.key:
root.right = bst_insert(root.right, key, val)
root.right.parent = root
return root
def bst_delete(root, key):
"""
Delete key from BST. Returns new root.
Time: O(h)
"""
if root is None:
return None
if key < root.key:
root.left = bst_delete(root.left, key)
elif key > root.key:
root.right = bst_delete(root.right, key)
else:
# Case 1: No children (leaf)
if root.left is None and root.right is None:
return None
# Case 2: One child
if root.left is None:
return root.right
if root.right is None:
return root.left
# Case 3: Two children — replace with inorder successor
successor = bst_min(root.right)
root.key = successor.key
root.val = successor.val
root.right = bst_delete(root.right, successor.key)
return root
def bst_min(node):
"""Find minimum key node. O(h)."""
while node.left:
node = node.left
return node
graph TD
    subgraph "Delete node 7 (two children)"
        A["Before: 5"] --> B[3]
        A --> C[7]
        B --> D[1]
        B --> E[4]
        C --> F[6]
        C --> G[9]
    end

    subgraph "After: replace with inorder successor 9"
        A2["After: 5"] --> B2[3]
        A2 --> C2[9]
        B2 --> D2[1]
        B2 --> E2[4]
        C2 --> F2[6]
    end
ShapeHeightSearch/Insert/Delete
BalancedO(logn)O(\log n)O(logn)O(\log n)
DegenerateO(n)O(n)O(n)O(n)
RandomO(logn)O(\log n) expectedO(logn)O(\log n) expected

For nn distinct keys inserted in random order, the expected height of a BST is approximately 2lnn1.39log2n2 \ln n \approx 1.39 \log_2 n. This is only about 39% taller than a perfectly balanced tree, but The worst case (sorted input) gives height nn.

def inorder(root):
"""Left -> Root -> Right. Yields sorted order for BST. O(n)."""
if root:
yield from inorder(root.left)
yield root.key
yield from inorder(root.right)
def preorder(root):
"""Root -> Left -> Right. O(n)."""
if root:
yield root.key
yield from preorder(root.left)
yield from preorder(root.right)
def postorder(root):
"""Left -> Right -> Root. O(n)."""
if root:
yield from postorder(root.left)
yield from postorder(root.right)
yield root.key
def level_order(root):
"""Breadth-first traversal. O(n)."""
from collections import deque
if root is None:
return
q = deque([root])
while q:
node = q.popleft()
yield node.key
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
def bst_successor(node):
"""
Find inorder successor of node.
Time: O(h)
"""
if node.right:
return bst_min(node.right)
parent = node.parent
while parent and node == parent.right:
node = parent
parent = parent.parent
return parent
def bst_predecessor(node):
"""
Find inorder predecessor of node.
Time: O(h)
"""
if node.left:
curr = node.left
while curr.right:
curr = curr.right
return curr
parent = node.parent
while parent and node == parent.left:
node = parent
parent = parent.parent
return parent

An AVL tree (Adelson-Velsky and Landis, 1962) is a self-balancing BST where the balance factor Of every node is in {1,0,1}\{-1, 0, 1\}. The balance factor is the height of the right subtree minus the Height of the left subtree.

\mathrm{bf(v) = \mathrm{height(v.\mathrm{right) - \mathrm{height(v.\mathrm{left)

After every insertion or deletion, we walk back up from the modified node to the root, rebalancing As needed. The balance factor must be in {1,0,1}\{-1, 0, 1\} for every node.

graph TD
    subgraph "Right Rotation (LL case)"
        A["z (bf=-2)"] --> B["y (bf=-1)"]
        A -.-> C["T3"]
        B --> D["T1"]
        B --> E["T2"]
    end

LL (Left-Left) Case: Right rotation on zz.

z y
/ \ / \
y T3 => T1 z
/ \ / \
T1 T2 T2 T3

RR (Right-Right) Case: Left rotation on zz.

z y
/ \ / \
T1 y => z T3
/ \ / \
T2 T3 T1 T2

LR (Left-Right) Case: Left rotation on yyThen right rotation on zz.

z z x
/ \ / \ / \
y T3 => x T3 => y z
/ \ / \ / \ / \
T1 x y T2 T1 T2 T3 T4
/ \ / \
T2 T3 T1 T2

RL (Right-Left) Case: Right rotation on yyThen left rotation on zz.

z z x
/ \ / \ / \
T1 y => T1 x => z y
/ \ / \ / \ / \
x T4 T2 y T1 T2 T3 T4
/ \ / \
T2 T3 T3 T4
class AVLNode:
def __init__(self, key, val=None):
self.key = key
self.val = val
self.left = None
self.right = None
self.height = 1
class AVLTree:
"""
AVL tree: self-balancing BST.
Search: O(log n) worst case
Insert: O(log n) worst case (at most 2 rotations)
Delete: O(log n) worst case (at most O(log n) rotations)
Space: O(n)
"""
def _height(self, node):
return node.height if node else 0
def _balance_factor(self, node):
if node is None:
return 0
return self._height(node.right) - self._height(node.left)
def _update_height(self, node):
node.height = 1 + max(self._height(node.left), self._height(node.right))
def _rotate_right(self, z):
y = z.left
T3 = y.right
y.right = z
z.left = T3
self._update_height(z)
self._update_height(y)
return y
def _rotate_left(self, z):
y = z.right
T2 = y.left
y.left = z
z.right = T2
self._update_height(z)
self._update_height(y)
return y
def _rebalance(self, node):
self._update_height(node)
bf = self._balance_factor(node)
if bf > 1:
if self._balance_factor(node.right) < 0:
node.right = self._rotate_right(node.right)
return self._rotate_left(node)
if bf < -1:
if self._balance_factor(node.left) > 0:
node.left = self._rotate_left(node.left)
return self._rotate_right(node)
return node
def insert(self, root, key, val=None):
if root is None:
return AVLNode(key, val)
if key < root.key:
root.left = self.insert(root.left, key, val)
elif key > root.key:
root.right = self.insert(root.right, key, val)
return self._rebalance(root)
def delete(self, root, key):
if root is None:
return None
if key < root.key:
root.left = self.delete(root.left, key)
elif key > root.key:
root.right = self.delete(root.right, key)
else:
if root.left is None:
return root.right
if root.right is None:
return root.left
successor = root.right
while successor.left:
successor = successor.left
root.key = successor.key
root.val = successor.val
root.right = self.delete(root.right, successor.key)
return self._rebalance(root)
def search(self, root, key):
curr = root
while curr:
if key == curr.key:
return curr
elif key < curr.key:
curr = curr.left
else:
curr = curr.right
return None

An AVL tree with nn nodes has height at most 1.44log2(n+2)1.3281.44 \log_2(n+2) - 1.328. Proof sketch: the minimum Number of nodes in an AVL tree of height hh is N(h)=N(h1)+N(h2)+1N(h) = N(h-1) + N(h-2) + 1 with N(0)=1N(0) = 1 N(1)=2N(1) = 2. This is closely related to the Fibonacci sequence, giving N(h)=Fh+31N(h) = F_{h+3} - 1. Since Fkϕk/5F_k \approx \phi^k / \sqrt{5}We get hclogϕ(n)h \le c \log_\phi(n) for some constant cc.

OperationWorst CaseRotations per InsertRotations per Delete
SearchO(logn)O(\log n)00
InsertO(logn)O(\log n)2\le 20
DeleteO(logn)O(\log n)0O(logn)O(\log n)