Skip to content

Advanced Trees

Python implementation β€” 4 questions covering core concepts and interview patterns.

ADVANCED TREES β€” Core Concepts & Interview Questions


"""
ADVANCED TREES β€” Core Concepts & Interview Questions
=====================================================

Core Concepts:
──────────────
Self-balancing trees maintain O(log n) height for all operations:
β€’ AVL Tree: Strictly height-balanced (|left_height - right_height| ≀ 1)
  - Rotations: LL, RR, LR, RL
  - Shorter than red-black β†’ slightly faster lookups; more rotations on
    writes. Used when lookups dominate
β€’ Red-Black Tree: Loosely balanced using color constraints
  - Root is black, no adjacent reds, equal black-height
  - Used in Java TreeMap/TreeSet (and HashMap's treeified buckets),
    C++ std::map in the common standard libraries, the Linux kernel
    (CFS scheduler run queue, epoll)
  - Insert: O(log n) with at most 2 rotations; delete at most 3

Multi-way Trees:
────────────────
β€’ B-Tree: Generalization of BST with multiple keys per node
  - All leaves at same depth
  - Every node (except root) has between ceil(m/2)-1 and m-1 keys
  - Node size = one disk/SSD page, so each level costs one page read
β€’ B+ Tree: B-Tree variant where only leaves store data
  - Internal nodes store keys only (routing table)
  - Leaves form a linked list for range queries
  - What databases actually mean by "B-tree index": MySQL InnoDB
    (clustered index), PostgreSQL nbtree (a Lehman-Yao B-link tree),
    SQLite tables; also filesystems such as Btrfs, XFS and NTFS
β€’ Write-heavy alternative: LSM trees (RocksDB, Cassandra) trade read
  cost for sequential writes

Comparison:
───────────
| Feature | AVL | Red-Black | B-Tree | B+ Tree |
|---------|-----|-----------|--------|---------|
| Height | ≀ 1.44 logβ‚‚ n | ≀ 2 logβ‚‚(n+1) | O(log_t n) | O(log_t n) |
| Lookup | O(log n) | O(log n) | O(log n) | O(log n) |
| Insert | O(log n) | O(log n) | O(log n) | O(log n) |
| Delete | O(log n) | O(log n) | O(log n) | O(log n) |
| Cache Misses | High | High | Low | Lowest |
| Range (k results) | O(log n + k) | O(log n + k) | O(log n + k) | O(log n + k) |

All four answer a range query in O(log n + k) via an in-order walk.
The B+ tree's real advantage is I/O: after one descent, the k results
are read from consecutive leaf pages via sibling links, while the
B-tree's in-order walk keeps bouncing between internal and leaf pages.
(t = minimum degree / branching factor, typically hundreds per page.)
"""

from typing import List, Optional, Tuple, Union
from enum import Enum


# ════════════════════════════════════════════════════════════════════════
# AVL TREE β€” Self-Balancing BST with Height Balance
# ════════════════════════════════════════════════════════════════════════

class AVLNode:
    """Node for AVL Tree."""
    def __init__(self, key: int):
        self.key = key
        self.left: Optional['AVLNode'] = None
        self.right: Optional['AVLNode'] = None
        self.height = 1  # Leaf nodes have height 1


class AVLTree:
    """
    AVL Tree Implementation.

    Core Properties:
    ───────────────
    β€’ For every node: |height(left) - height(right)| ≀ 1
    β€’ Balance factor = height(left) - height(right)
    β€’ On violation: perform rotations to restore balance

    Rotations:
    ──────────
    β€’ LL (Right Rotate): Insert into left of left-heavy node
    β€’ RR (Left Rotate): Insert into right of right-heavy node
    β€’ LR (Left-Right): Insert into right of left-heavy β†’ left rotate on child,
      then right rotate on parent
    β€’ RL (Right-Left): Insert into left of right-heavy β†’ right rotate on child,
      then left rotate on parent

    COMPLEXITY:
    ──────────
    Search: O(log n)
    Insert: O(log n) β€” with at most 1-2 rotations
    Delete: O(log n) β€” with O(log n) rotations (rebalancing up the tree)
    Space: O(n)
    """

    def __init__(self):
        self.root: Optional[AVLNode] = None

    def _height(self, node: Optional[AVLNode]) -> int:
        return node.height if node else 0

    def _balance_factor(self, node: Optional[AVLNode]) -> int:
        if not node:
            return 0
        return self._height(node.left) - self._height(node.right)

    def _update_height(self, node: AVLNode) -> None:
        node.height = 1 + max(self._height(node.left), self._height(node.right))

    def _rotate_right(self, y: AVLNode) -> AVLNode:
        r"""
        Right rotation (for LL imbalance):
            y               x
           / \             / \
          x   T3    β†’     T1  y
         / \                 / \
        T1  T2              T2  T3
        """
        x = y.left
        T2 = x.right

        x.right = y
        y.left = T2

        self._update_height(y)
        self._update_height(x)

        return x

    def _rotate_left(self, x: AVLNode) -> AVLNode:
        r"""
        Left rotation (for RR imbalance):
            x               y
           / \             / \
          T1  y     β†’     x   T3
             / \         / \
            T2  T3      T1  T2
        """
        y = x.right
        T2 = y.left

        y.left = x
        x.right = T2

        self._update_height(x)
        self._update_height(y)

        return y

    def insert(self, key: int) -> None:
        """Insert a key into the AVL tree."""
        self.root = self._insert(self.root, key)

    def _insert(self, node: Optional[AVLNode], key: int) -> AVLNode:
        # 1. Standard BST insert
        if not node:
            return AVLNode(key)

        if key < node.key:
            node.left = self._insert(node.left, key)
        elif key > node.key:
            node.right = self._insert(node.right, key)
        else:
            return node  # No duplicates

        # 2. Update height
        self._update_height(node)

        # 3. Get balance factor and rebalance
        balance = self._balance_factor(node)

        # Left-Left (LL) Case
        if balance > 1 and key < node.left.key:
            return self._rotate_right(node)

        # Right-Right (RR) Case
        if balance < -1 and key > node.right.key:
            return self._rotate_left(node)

        # Left-Right (LR) Case
        if balance > 1 and key > node.left.key:
            node.left = self._rotate_left(node.left)
            return self._rotate_right(node)

        # Right-Left (RL) Case
        if balance < -1 and key < node.right.key:
            node.right = self._rotate_right(node.right)
            return self._rotate_left(node)

        return node

    def delete(self, key: int) -> None:
        """Delete a key from the AVL tree."""
        self.root = self._delete(self.root, key)

    def _min_value_node(self, node: AVLNode) -> AVLNode:
        """Find the node with minimum key in the subtree."""
        current = node
        while current.left:
            current = current.left
        return current

    def _delete(self, node: Optional[AVLNode], key: int) -> Optional[AVLNode]:
        # 1. Standard BST delete
        if not node:
            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:
            # Node with only one child or no child
            if not node.left:
                return node.right
            elif not node.right:
                return node.left

            # Node with two children: get inorder successor
            successor = self._min_value_node(node.right)
            node.key = successor.key
            node.right = self._delete(node.right, successor.key)

        if not node:
            return None

        # 2. Update height
        self._update_height(node)

        # 3. Rebalance
        balance = self._balance_factor(node)

        # Left-Left
        if balance > 1 and self._balance_factor(node.left) >= 0:
            return self._rotate_right(node)

        # Left-Right
        if balance > 1 and self._balance_factor(node.left) < 0:
            node.left = self._rotate_left(node.left)
            return self._rotate_right(node)

        # Right-Right
        if balance < -1 and self._balance_factor(node.right) <= 0:
            return self._rotate_left(node)

        # Right-Left
        if balance < -1 and self._balance_factor(node.right) > 0:
            node.right = self._rotate_right(node.right)
            return self._rotate_left(node)

        return node

    def search(self, key: int) -> bool:
        """Search for a key in the AVL tree."""
        current = self.root
        while current:
            if key == current.key:
                return True
            elif key < current.key:
                current = current.left
            else:
                current = current.right
        return False

    def inorder(self) -> List[int]:
        """Return inorder traversal (sorted order)."""
        result = []
        self._inorder(self.root, result)
        return result

    def _inorder(self, node: Optional[AVLNode], result: List[int]) -> None:
        if node:
            self._inorder(node.left, result)
            result.append(node.key)
            self._inorder(node.right, result)


# ════════════════════════════════════════════════════════════════════════
# RED-BLACK TREE (Conceptual Implementation)
# ════════════════════════════════════════════════════════════════════════

class Color(Enum):
    RED = 0
    BLACK = 1


class RBNode:
    """Node for Red-Black Tree."""
    def __init__(self, key: int):
        self.key = key
        self.color = Color.RED  # New nodes are always red
        self.left: Optional['RBNode'] = None
        self.right: Optional['RBNode'] = None
        self.parent: Optional['RBNode'] = None


class RedBlackTree:
    """
    Red-Black Tree Implementation.

    Properties:
    ──────────
    1. Every node is either red or black
    2. Root is always black
    3. Leaves (NIL) are black
    4. No two adjacent red nodes (red node's children are black)
    5. Every path from root to leaf has same number of black nodes

    THOUGHT PROCESS:
    ────────────────
    Insert fixes a potential violation of property 4 (red-red conflict).
    The fix depends on the uncle's color:
    β€’ Uncle is RED β†’ Recolor (no rotation needed)
    β€’ Uncle is BLACK β†’ Rotate + Recolor (same patterns as AVL)

    COMPLEXITY:
    ──────────
    Search: O(log n)
    Insert: O(log n) β€” at most 2 rotations
    Delete: O(log n) β€” at most 3 rotations
    Space: O(n)
    """

    def __init__(self):
        self.NIL = RBNode(0)
        self.NIL.color = Color.BLACK
        self.NIL.left = None
        self.NIL.right = None
        self.NIL.parent = None
        self.root = self.NIL

    def _rotate_left(self, x: RBNode) -> None:
        """Left rotate around node x."""
        y = x.right
        x.right = y.left
        if y.left != self.NIL:
            y.left.parent = x
        y.parent = x.parent
        if x.parent == self.NIL:
            self.root = y
        elif x == x.parent.left:
            x.parent.left = y
        else:
            x.parent.right = y
        y.left = x
        x.parent = y

    def _rotate_right(self, x: RBNode) -> None:
        """Right rotate around node x."""
        y = x.left
        x.left = y.right
        if y.right != self.NIL:
            y.right.parent = x
        y.parent = x.parent
        if x.parent == self.NIL:
            self.root = y
        elif x == x.parent.right:
            x.parent.right = y
        else:
            x.parent.left = y
        y.right = x
        x.parent = y

    def insert(self, key: int) -> None:
        """Insert a key into the Red-Black tree."""
        new_node = RBNode(key)
        new_node.left = self.NIL
        new_node.right = self.NIL

        # Standard BST insert
        y = self.NIL
        x = self.root

        while x != self.NIL:
            y = x
            if new_node.key < x.key:
                x = x.left
            else:
                x = x.right

        new_node.parent = y
        if y == self.NIL:
            self.root = new_node
        elif new_node.key < y.key:
            y.left = new_node
        else:
            y.right = new_node

        # Fix Red-Black properties
        self._insert_fixup(new_node)

    def _insert_fixup(self, z: RBNode) -> None:
        """Fix red-black tree after insertion."""
        while z.parent.color == Color.RED:
            if z.parent == z.parent.parent.left:
                y = z.parent.parent.right  # Uncle
                if y.color == Color.RED:
                    # Case 1: Uncle is red β†’ recolor
                    z.parent.color = Color.BLACK
                    y.color = Color.BLACK
                    z.parent.parent.color = Color.RED
                    z = z.parent.parent
                else:
                    # Case 2/3: Uncle is black β†’ rotate
                    if z == z.parent.right:
                        # Case 2: z is right child β†’ left rotate
                        z = z.parent
                        self._rotate_left(z)
                    # Case 3: z is left child β†’ right rotate + recolor
                    z.parent.color = Color.BLACK
                    z.parent.parent.color = Color.RED
                    self._rotate_right(z.parent.parent)
            else:
                # Symmetric case: parent is right child
                y = z.parent.parent.left  # Uncle
                if y.color == Color.RED:
                    z.parent.color = Color.BLACK
                    y.color = Color.BLACK
                    z.parent.parent.color = Color.RED
                    z = z.parent.parent
                else:
                    if z == z.parent.left:
                        z = z.parent
                        self._rotate_right(z)
                    z.parent.color = Color.BLACK
                    z.parent.parent.color = Color.RED
                    self._rotate_left(z.parent.parent)

        self.root.color = Color.BLACK

    def search(self, key: int) -> bool:
        """Search for a key in the Red-Black tree."""
        current = self.root
        while current != self.NIL:
            if key == current.key:
                return True
            elif key < current.key:
                current = current.left
            else:
                current = current.right
        return False

    def inorder(self) -> List[int]:
        """Return inorder traversal (sorted order)."""
        result = []
        self._inorder(self.root, result)
        return result

    def _inorder(self, node: RBNode, result: List[int]) -> None:
        if node != self.NIL:
            self._inorder(node.left, result)
            result.append(node.key)
            self._inorder(node.right, result)


# ════════════════════════════════════════════════════════════════════════
# B-TREE β€” Balanced Multi-way Search Tree
# ════════════════════════════════════════════════════════════════════════

class BTreeNode:
    """Node for B-Tree of order m (maximum children)."""
    def __init__(self, is_leaf: bool = True):
        self.keys: List[int] = []
        self.children: List['BTreeNode'] = []
        self.is_leaf = is_leaf


class BTree:
    """
    B-Tree of order m (minimum degree = t, max keys = 2t-1).

    Properties:
    ──────────
    β€’ All leaves are at the same depth
    β€’ Every node (except root) has at least t-1 keys
    β€’ Every node (except root) has at most 2t-1 keys
    β€’ Children count = keys count + 1 (for internal nodes)

    THOUGHT PROCESS:
    ────────────────
    β€’ Insert: Find the leaf, insert in sorted position.
      If node is full (2t-1 keys), split before descending.
      Split: median key goes up, left and right halves become children.
    β€’ Search: Linear scan within node, then recurse to appropriate child.
    β€’ This "preemptive split" approach avoids needing to split on
      the way back up.

    COMPLEXITY:
    ──────────
    Search: O(log_t n) node visits (page reads); O(log n) comparisons
            in total with binary search inside each node
    Insert: O(t Β· log_t n) CPU β€” shifting keys within each split node β€”
            but still O(log_t n) page reads, which is what matters on disk
    Delete: O(log_t n) page reads (borrow from / merge with siblings)
    Space: O(n) β€” every non-root node is at least half full

    NOTE: this demo B-tree accepts duplicate keys and uses a linear scan
    inside nodes; real implementations binary-search each page.
    """

    def __init__(self, t: int = 3):
        self.root = BTreeNode(is_leaf=True)
        self.t = t  # Minimum degree

    def search(self, key: int) -> bool:
        """Search for a key in the B-Tree."""
        return self._search(self.root, key)

    def _search(self, node: BTreeNode, key: int) -> bool:
        i = 0
        while i < len(node.keys) and key > node.keys[i]:
            i += 1

        if i < len(node.keys) and key == node.keys[i]:
            return True

        if node.is_leaf:
            return False

        return self._search(node.children[i], key)

    def insert(self, key: int) -> None:
        """Insert a key into the B-Tree."""
        root = self.root
        # If root is full, grow the tree upward
        if len(root.keys) == 2 * self.t - 1:
            new_root = BTreeNode(is_leaf=False)
            new_root.children.append(self.root)
            self._split_child(new_root, 0)
            self.root = new_root

        self._insert_non_full(self.root, key)

    def _insert_non_full(self, node: BTreeNode, key: int) -> None:
        i = len(node.keys) - 1

        if node.is_leaf:
            # Insert key in sorted position
            node.keys.append(0)  # Extend the list
            while i >= 0 and key < node.keys[i]:
                node.keys[i + 1] = node.keys[i]
                i -= 1
            node.keys[i + 1] = key
        else:
            # Find the child to descend into
            while i >= 0 and key < node.keys[i]:
                i -= 1
            i += 1

            # If child is full, split it first
            if len(node.children[i].keys) == 2 * self.t - 1:
                self._split_child(node, i)
                # After split, decide which child to go to
                if key > node.keys[i]:
                    i += 1

            self._insert_non_full(node.children[i], key)

    def _split_child(self, parent: BTreeNode, i: int) -> None:
        """Split the i-th child of parent (which is full)."""
        t = self.t
        y = parent.children[i]  # Full child to split
        z = BTreeNode(is_leaf=y.is_leaf)

        # Save median key (at position t-1 in the full node)
        median_key = y.keys[t - 1]

        # z gets the largest t-1 keys (indices t to 2t-2)
        z.keys = y.keys[t:]

        # z gets the corresponding children (if not leaf)
        if not y.is_leaf:
            z.children = y.children[t:]

        # y keeps the smallest t-1 keys (indices 0 to t-2)
        y.keys = y.keys[:t - 1]
        if not y.is_leaf:
            y.children = y.children[:t]

        # Insert median key into parent at position i
        parent.keys.insert(i, median_key)
        # Add z as the next child in parent
        parent.children.insert(i + 1, z)

    def inorder(self) -> List[int]:
        """Return inorder traversal (sorted order)."""
        result = []
        self._inorder(self.root, result)
        return result

    def _inorder(self, node: BTreeNode, result: List[int]) -> None:
        if node:
            for i in range(len(node.keys)):
                if not node.is_leaf:
                    self._inorder(node.children[i], result)
                result.append(node.keys[i])
            if not node.is_leaf:
                self._inorder(node.children[len(node.keys)], result)


# ════════════════════════════════════════════════════════════════════════
# B+ TREE (Conceptual Explanation)
# ════════════════════════════════════════════════════════════════════════

"""
B+ TREE β€” Conceptual Overview
==============================

The B+ Tree is a B-Tree variant optimized for database indexing.
Only leaf nodes store data pointers; internal nodes act as routers.

Key Differences from B-Tree:
────────────────────────────
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Feature           β”‚ B-Tree             β”‚ B+ Tree              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Data pointers     β”‚ All nodes          β”‚ Leaf nodes only      β”‚
β”‚ Internal nodes    β”‚ Store keys + data  β”‚ Store keys only      β”‚
β”‚ Leaf structure    β”‚ Scattered          β”‚ Linked list chain    β”‚
β”‚ Range queries     β”‚ O(log n + k), but  β”‚ O(log n + k), with   β”‚
β”‚                    β”‚ random page reads  β”‚ sequential leaf readsβ”‚
β”‚ Cache efficiency  β”‚ Lower              β”‚ Higher (more keys    β”‚
β”‚                    β”‚                     β”‚ per internal node)   β”‚
β”‚ Fan-out          β”‚ Lower              β”‚ Higher               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Structure:
─────────
β€’ Internal nodes: [k1, k2, ..., kn], each key is a routing value
  - Children: c0, c1, ..., cn where c_i holds keys in [k_i, k_{i+1})
    (c0 holds keys < k1)
β€’ Leaf nodes: [k1:v1, k2:v2, ..., kn:vn] β€” key-value pairs
  - Leaves are linked: leaf.next = next_leaf (for range scans)
β€’ All leaves are at the same depth

Operations:
───────────
β€’ Search: Traverse from root, follow routing keys to leaf, scan leaf
β€’ Insert: Find leaf, insert in sorted position. If leaf overflow:
  1. Split leaf: half keys stay, half go to new leaf
  2. Copy median key (first key of new leaf) up to parent
  3. If parent overflows, split and propagate up
β€’ Delete: Find and remove. If leaf underflow:
  1. Try borrowing from sibling
  2. If can't borrow, merge with sibling
  3. Update parent key, propagate underflow

Why B+ Tree Wins for Databases:
───────────────────────────────
1. Higher fan-out = shorter tree = fewer disk seeks
   - B+ Tree with order 100: 100^4 = 100M records in 4 levels; real
     fan-out with 8-16 KB pages is often several hundred, so 3-4
     levels cover billions of rows
2. Range queries: just walk the leaf linked list
   - B-Tree's in-order walk climbs back into internal nodes (more,
     non-sequential page reads)
3. Better cache locality: the few internal levels stay in the buffer
   pool, so a lookup usually costs ~1 physical read (the leaf)
4. Internal nodes store only keys (more keys per node)

What interviewers probe next:
β€’ Clustered vs secondary index (InnoDB secondary leaves store the
  primary key β†’ a second lookup unless the index is covering)
β€’ Why random UUID primary keys hurt: inserts land on random leaves β†’
  page splits and poor cache locality; time-ordered IDs (UUIDv7) fix it
β€’ Concurrency: latch crabbing / B-link trees (PostgreSQL) so readers
  don't block on splits
"""


# ════════════════════════════════════════════════════════════════════════
# DEMO
# ════════════════════════════════════════════════════════════════════════

def demo():
    print("=" * 70)
    print("ADVANCED TREES β€” Interview Questions Demo")
    print("=" * 70)

    # AVL Tree
    print("\n🌳 AVL TREE")
    print("-" * 40)
    avl = AVLTree()
    keys = [10, 20, 30, 40, 50, 25]
    for k in keys:
        avl.insert(k)
        print(f"   Insert {k}: inorder = {avl.inorder()}")
    print(f"   Search 25: {avl.search(25)}")
    print(f"   Search 100: {avl.search(100)}")
    avl.delete(40)
    print(f"   After delete 40: {avl.inorder()}")
    avl.delete(50)
    print(f"   After delete 50: {avl.inorder()}")
    print(f"   Height: {avl._height(avl.root)}")

    # Red-Black Tree
    print("\nπŸ”΄βš« RED-BLACK TREE")
    print("-" * 40)
    rbt = RedBlackTree()
    for k in [10, 20, 30, 15, 25, 5, 1]:
        rbt.insert(k)
    print(f"   Insert [10,20,30,15,25,5,1]: {rbt.inorder()}")
    print(f"   Search 15: {rbt.search(15)}")
    print(f"   Search 100: {rbt.search(100)}")

    # B-Tree
    print("\n🌲 B-TREE (min degree t=3)")
    print("-" * 40)
    bt = BTree(t=3)
    for k in [10, 20, 5, 6, 12, 30, 7, 17]:
        bt.insert(k)
    print(f"   Inserted [10,20,5,6,12,30,7,17]")
    inorder = bt.inorder()
    print(f"   Inorder: {inorder}")
    print(f"   Sorted? {inorder == sorted(inorder)}")
    print(f"   Search 17: {bt.search(17)}")
    print(f"   Search 99: {bt.search(99)}")

    # B+ Tree Concept
    print("\nπŸ—„οΈ  B+ TREE (Conceptual)")
    print("-" * 40)
    print("   See docstring for detailed explanation")
    print("   Key advantage: Range queries via leaf linked list")
    print("   Used in: MySQL InnoDB, PostgreSQL (nbtree), SQLite, many filesystems")
    print("   Fan-out of 100: 100^4 = 100M records in 4 levels")

    print("\n" + "=" * 70)


if __name__ == "__main__":
    demo()

← Back to DSA Overview