Skip to content

AVL trees

Problem

Implement an AVL tree storing distinct integers, with insertion, deletion, and search while maintaining BST order and a balance factor between \(-1\) and \(1\) at every node.

insert [30, 20, 10, 25] -> delete 10 -> keys [20, 25, 30], root 25, search(20) = True

Key trick

After every recursive insertion or deletion, update the node's height and use single or double rotations to restore balance.

Trap

  • Updating heights in the wrong order during rotations.
  • Forgetting double rotations for left-right and right-left cases.
  • Failing to rebalance every ancestor after deletion.
  • Losing the new subtree root returned by recursion or rotation.

Why is it interesting?

AVL trees preserve BST ordering using only local rotations, guaranteeing \(O(\log n)\) worst-case search, insertion, and deletion.

Python solution

class AVLTree:
    class Node:
        def __init__(self, key: int):
            self.key = key
            self.left = None
            self.right = None
            self.height = 1

    def __init__(self):
        self.root = None

    def _height(self, node):
        return node.height if node else 0

    def _update(self, node):
        node.height = 1 + max(
            self._height(node.left),
            self._height(node.right),
        )

    def _balance_factor(self, node):
        return self._height(node.left) - self._height(node.right)

    def _rotate_right(self, node):
        root = node.left
        node.left = root.right
        root.right = node

        # Update the lower node before the new subtree root.
        self._update(node)
        self._update(root)
        return root

    def _rotate_left(self, node):
        root = node.right
        node.right = root.left
        root.left = node

        self._update(node)
        self._update(root)
        return root

    def _rebalance(self, node):
        self._update(node)
        balance = self._balance_factor(node)

        if balance > 1:
            # Left-right case: first convert it to left-left.
            if self._balance_factor(node.left) < 0:
                node.left = self._rotate_left(node.left)
            return self._rotate_right(node)

        if balance < -1:
            # Right-left case: first convert it to right-right.
            if self._balance_factor(node.right) > 0:
                node.right = self._rotate_right(node.right)
            return self._rotate_left(node)

        return node

    def _insert(self, node, key):
        if node is None:
            return self.Node(key)

        if key < node.key:
            node.left = self._insert(node.left, key)
        elif key > node.key:
            node.right = self._insert(node.right, key)
        else:
            # Duplicate keys are ignored.
            return node

        return self._rebalance(node)

    def insert(self, key: int) -> None:
        self.root = self._insert(self.root, key)

    def _min_node(self, node):
        while node.left:
            node = node.left
        return node

    def _delete(self, node, key):
        if node is None:
            return None

        if key < node.key:
            node.left = self._delete(node.left, key)
        elif key > node.key:
            node.right = self._delete(node.right, key)
        else:
            if node.left is None:
                return node.right

            if node.right is None:
                return node.left

            # Replace the key with its inorder successor, then remove it.
            successor = self._min_node(node.right)
            node.key = successor.key
            node.right = self._delete(node.right, successor.key)

        return self._rebalance(node)

    def delete(self, key: int) -> None:
        self.root = self._delete(self.root, key)

    def search(self, key: int) -> bool:
        node = self.root

        while node:
            if key == node.key:
                return True

            node = node.left if key < node.key else node.right

        return False
  • Search, insertion, and deletion: \(O(\log n)\) time.
  • Search: \(O(1)\) auxiliary space.
  • Insertion and deletion: \(O(\log n)\) recursion space.
  • Tree storage: \(O(n)\).