Binary Search Trees

19 min read

Binary Search Trees (BST)

1. Summary / TL;DR

  • BST Property: For every node, all left descendants < node < all right descendants
  • Inorder traversal of BST yields sorted sequence
  • Average case: O(log n) for search, insert, delete; Worst case: O(n) for skewed trees
  • Common operations: search, insert, delete, find min/max, successor/predecessor
  • Balanced BSTs (AVL, Red-Black) guarantee O(log n) worst case
  • Key patterns: validate BST, kth smallest, LCA, range queries

2. When & Where to Use

ScenarioWhy BST?
Ordered data with frequent search/insert/deleteO(log n) average for all ops
Finding min/max elementsO(h) traversal to leftmost/rightmost
Range queriesEfficient in-range element retrieval
Successor/predecessor queriesO(h) navigation
Dynamic sorted dataInsert maintains sorted order
Implementing TreeSet/TreeMapUnderlying data structure

BST vs Hash Table

AspectBSTHash Table
SearchO(log n) avgO(1) avg
Insert/DeleteO(log n) avgO(1) avg
Ordered iterationO(n)Not available
Range queriesEfficientMust check all
SpaceO(n)O(n) + overhead
Min/MaxO(log n)O(n)

3. Time & Space Complexity

BST Operations

OperationAverageWorst (Skewed)Balanced BST
SearchO(log n)O(n)O(log n)
InsertO(log n)O(n)O(log n)
DeleteO(log n)O(n)O(log n)
Find Min/MaxO(log n)O(n)O(log n)
Successor/PredecessorO(log n)O(n)O(log n)
Inorder TraversalO(n)O(n)O(n)

Space: O(n) for storing n nodes, O(h) for recursion stack


4. Core Concepts & Theory

BST Property

           8           For every node N:
         /   \         - All nodes in left subtree < N
        3     10       - All nodes in right subtree > N
       / \      \
      1   6      14
         / \    /
        4   7  13

Inorder: 1, 3, 4, 6, 7, 8, 10, 13, 14 (sorted!)

BST Properties

  1. No duplicates (typically) - duplicates can go left, right, or use count
  2. Inorder = Sorted - key insight for many problems
  3. Search = Binary Search on Tree - eliminate half at each step
  4. Height matters - determines efficiency; ranges from O(log n) to O(n)

Successor and Predecessor

Inorder Successor (next larger element):

  • If node has right child: leftmost node in right subtree
  • Else: lowest ancestor for which node is in left subtree

Inorder Predecessor (previous smaller element):

  • If node has left child: rightmost node in left subtree
  • Else: lowest ancestor for which node is in right subtree
        20
       /  \
      10   30
     /  \
    5   15

Successor of 15: 20 (go up to first ancestor where 15 is in left subtree)
Successor of 10: 15 (rightmost in left subtree of 10's right child)
Predecessor of 20: 15 (rightmost in left subtree)

5. Diagrams / Visualizations

BST Search Process

Search for 7 in BST:

           8            Step 1: 7 < 8, go left
         /   \
       [3]    10        Step 2: 7 > 3, go right
       / \      \
      1  [6]    14      Step 3: 7 > 6, go right
         / \    /
        4  [7] 13       Step 4: Found 7!

Path: 8 → 3 → 6 → 7
Comparisons: 4 (height of tree + 1)

BST Deletion Cases

Case 1: Leaf node (no children)
Delete 4:
    6           6
   / \    →    / \
  4   7           7

Case 2: One child
Delete 3:
    8           8
   / \         / \
  3   10  →   6   10
   \
    6

Case 3: Two children (replace with inorder successor)
Delete 8:
    8                10  (successor)
   / \              /  \
  3   10    →      3    14
       \              /
        14          13
       /
      13

6. Implementation (Python)

BST Node Definition

from typing import Optional, List, Iterator

class BSTNode:
    """Binary Search Tree node."""
    def __init__(self, val: int):
        self.val = val
        self.left: Optional['BSTNode'] = None
        self.right: Optional['BSTNode'] = None
    
    def __repr__(self) -> str:
        return f"BSTNode({self.val})"

Complete BST Class

class BinarySearchTree:
    """
    Binary Search Tree implementation with standard operations.
    Assumes no duplicate values.
    """
    
    def __init__(self):
        self.root: Optional[BSTNode] = None
        self.size: int = 0
    
    def search(self, val: int) -> Optional[BSTNode]:
        """
        Search for value in BST.
        Time: O(h) where h is height
        
        >>> bst = BinarySearchTree()
        >>> bst.insert(5); bst.insert(3); bst.insert(7)
        >>> bst.search(3).val
        3
        >>> bst.search(10) is None
        True
        """
        return self._search_recursive(self.root, val)
    
    def _search_recursive(self, node: Optional[BSTNode], val: int) -> Optional[BSTNode]:
        if not node or node.val == val:
            return node
        
        if val < node.val:
            return self._search_recursive(node.left, val)
        else:
            return self._search_recursive(node.right, val)
    
    def search_iterative(self, val: int) -> Optional[BSTNode]:
        """Iterative search - more space efficient."""
        current = self.root
        
        while current:
            if val == current.val:
                return current
            elif val < current.val:
                current = current.left
            else:
                current = current.right
        
        return None
    
    def insert(self, val: int) -> None:
        """
        Insert value into BST.
        Time: O(h)
        
        >>> bst = BinarySearchTree()
        >>> bst.insert(5); bst.insert(3); bst.insert(7)
        >>> bst.inorder()
        [3, 5, 7]
        """
        self.root = self._insert_recursive(self.root, val)
        self.size += 1
    
    def _insert_recursive(self, node: Optional[BSTNode], val: int) -> BSTNode:
        if not node:
            return BSTNode(val)
        
        if val < node.val:
            node.left = self._insert_recursive(node.left, val)
        else:  # val > node.val (assuming no duplicates)
            node.right = self._insert_recursive(node.right, val)
        
        return node
    
    def insert_iterative(self, val: int) -> None:
        """Iterative insert."""
        new_node = BSTNode(val)
        
        if not self.root:
            self.root = new_node
            self.size += 1
            return
        
        current = self.root
        while True:
            if val < current.val:
                if not current.left:
                    current.left = new_node
                    break
                current = current.left
            else:
                if not current.right:
                    current.right = new_node
                    break
                current = current.right
        
        self.size += 1
    
    def delete(self, val: int) -> None:
        """
        Delete value from BST.
        Time: O(h)
        
        >>> bst = BinarySearchTree()
        >>> for v in [5, 3, 7, 2, 4]: bst.insert(v)
        >>> bst.delete(3)
        >>> bst.inorder()
        [2, 4, 5, 7]
        """
        self.root = self._delete_recursive(self.root, val)
        self.size -= 1
    
    def _delete_recursive(self, node: Optional[BSTNode], val: int) -> Optional[BSTNode]:
        if not node:
            return None
        
        if val < node.val:
            node.left = self._delete_recursive(node.left, val)
        elif val > node.val:
            node.right = self._delete_recursive(node.right, val)
        else:
            # Found node to delete
            
            # Case 1: Leaf node
            if not node.left and not node.right:
                return None
            
            # Case 2: One child
            if not node.left:
                return node.right
            if not node.right:
                return node.left
            
            # Case 3: Two children
            # Replace with inorder successor (minimum in right subtree)
            successor = self._find_min_node(node.right)
            node.val = successor.val
            node.right = self._delete_recursive(node.right, successor.val)
        
        return node
    
    def find_min(self) -> Optional[int]:
        """Find minimum value in BST."""
        if not self.root:
            return None
        return self._find_min_node(self.root).val
    
    def _find_min_node(self, node: BSTNode) -> BSTNode:
        """Find node with minimum value."""
        current = node
        while current.left:
            current = current.left
        return current
    
    def find_max(self) -> Optional[int]:
        """Find maximum value in BST."""
        if not self.root:
            return None
        
        current = self.root
        while current.right:
            current = current.right
        return current.val
    
    def inorder(self) -> List[int]:
        """Return inorder traversal (sorted order)."""
        result = []
        self._inorder_recursive(self.root, result)
        return result
    
    def _inorder_recursive(self, node: Optional[BSTNode], result: List[int]) -> None:
        if node:
            self._inorder_recursive(node.left, result)
            result.append(node.val)
            self._inorder_recursive(node.right, result)
    
    def inorder_successor(self, val: int) -> Optional[int]:
        """
        Find inorder successor (next larger element).
        
        >>> bst = BinarySearchTree()
        >>> for v in [20, 10, 30, 5, 15, 25, 35]: bst.insert(v)
        >>> bst.inorder_successor(15)
        20
        >>> bst.inorder_successor(10)
        15
        """
        successor = None
        current = self.root
        
        while current:
            if val < current.val:
                successor = current.val  # This could be successor
                current = current.left
            else:
                current = current.right
        
        return successor
    
    def inorder_predecessor(self, val: int) -> Optional[int]:
        """Find inorder predecessor (previous smaller element)."""
        predecessor = None
        current = self.root
        
        while current:
            if val > current.val:
                predecessor = current.val  # This could be predecessor
                current = current.right
            else:
                current = current.left
        
        return predecessor
    
    def __len__(self) -> int:
        return self.size
    
    def __contains__(self, val: int) -> bool:
        return self.search(val) is not None

Validate BST (LeetCode 98)

def is_valid_bst(root: Optional[BSTNode]) -> bool:
    """
    Validate if tree is a valid BST.
    
    Approach: Each node must be within a valid range.
    Left subtree has upper bound, right subtree has lower bound.
    
    Time: O(n), Space: O(h)
    
    >>> root = BSTNode(5)
    >>> root.left, root.right = BSTNode(3), BSTNode(7)
    >>> is_valid_bst(root)
    True
    """
    def validate(node: Optional[BSTNode], 
                 min_val: float, 
                 max_val: float) -> bool:
        if not node:
            return True
        
        if not (min_val < node.val < max_val):
            return False
        
        return (validate(node.left, min_val, node.val) and
                validate(node.right, node.val, max_val))
    
    return validate(root, float('-inf'), float('inf'))


def is_valid_bst_inorder(root: Optional[BSTNode]) -> bool:
    """
    Validate BST using inorder traversal.
    Key insight: Inorder of valid BST is strictly increasing.
    
    >>> root = BSTNode(5)
    >>> root.left, root.right = BSTNode(3), BSTNode(7)
    >>> is_valid_bst_inorder(root)
    True
    """
    prev = float('-inf')
    
    def inorder(node: Optional[BSTNode]) -> bool:
        nonlocal prev
        
        if not node:
            return True
        
        # Check left subtree
        if not inorder(node.left):
            return False
        
        # Check current (must be greater than previous)
        if node.val <= prev:
            return False
        prev = node.val
        
        # Check right subtree
        return inorder(node.right)
    
    return inorder(root)

Kth Smallest Element (LeetCode 230)

def kth_smallest(root: Optional[BSTNode], k: int) -> int:
    """
    Find kth smallest element in BST.
    
    Approach: Inorder traversal visits elements in sorted order.
    Stop when we've visited k elements.
    
    Time: O(H + k) where H is height
    Space: O(H)
    
    >>> root = BSTNode(5)
    >>> root.left, root.right = BSTNode(3), BSTNode(7)
    >>> root.left.left, root.left.right = BSTNode(2), BSTNode(4)
    >>> kth_smallest(root, 3)  # sorted: 2,3,4,5,7
    4
    """
    count = 0
    result = None
    
    def inorder(node: Optional[BSTNode]) -> None:
        nonlocal count, result
        
        if not node or result is not None:
            return
        
        inorder(node.left)
        
        count += 1
        if count == k:
            result = node.val
            return
        
        inorder(node.right)
    
    inorder(root)
    return result


def kth_smallest_iterative(root: Optional[BSTNode], k: int) -> int:
    """Iterative approach using stack."""
    stack = []
    current = root
    count = 0
    
    while current or stack:
        # Go left as far as possible
        while current:
            stack.append(current)
            current = current.left
        
        current = stack.pop()
        count += 1
        
        if count == k:
            return current.val
        
        current = current.right
    
    return -1  # k > number of nodes

Lowest Common Ancestor in BST (LeetCode 235)

def lca_bst(root: Optional[BSTNode], p: int, q: int) -> Optional[BSTNode]:
    """
    Find LCA of two nodes in BST.
    
    Key insight: LCA is the first node where p and q diverge
    (one goes left, one goes right), or one of p/q itself.
    
    Time: O(h), Space: O(1) iterative
    
    >>> root = BSTNode(6)
    >>> root.left, root.right = BSTNode(2), BSTNode(8)
    >>> lca_bst(root, 2, 8).val
    6
    """
    current = root
    
    while current:
        if p < current.val and q < current.val:
            # Both in left subtree
            current = current.left
        elif p > current.val and q > current.val:
            # Both in right subtree
            current = current.right
        else:
            # Diverge here (or one of them is current)
            return current
    
    return None


def lca_bst_recursive(root: Optional[BSTNode], p: int, q: int) -> Optional[BSTNode]:
    """Recursive approach."""
    if not root:
        return None
    
    if p < root.val and q < root.val:
        return lca_bst_recursive(root.left, p, q)
    elif p > root.val and q > root.val:
        return lca_bst_recursive(root.right, p, q)
    else:
        return root

Convert Sorted Array to BST (LeetCode 108)

def sorted_array_to_bst(nums: List[int]) -> Optional[BSTNode]:
    """
    Convert sorted array to height-balanced BST.
    
    Approach: Middle element becomes root, recurse on left and right halves.
    
    Time: O(n), Space: O(log n) recursion
    
    >>> nums = [1, 2, 3, 4, 5, 6, 7]
    >>> root = sorted_array_to_bst(nums)
    >>> root.val  # Middle element
    4
    """
    def build(left: int, right: int) -> Optional[BSTNode]:
        if left > right:
            return None
        
        mid = (left + right) // 2
        node = BSTNode(nums[mid])
        
        node.left = build(left, mid - 1)
        node.right = build(mid + 1, right)
        
        return node
    
    return build(0, len(nums) - 1)

Range Sum BST (LeetCode 938)

def range_sum_bst(root: Optional[BSTNode], low: int, high: int) -> int:
    """
    Sum of all nodes with values in range [low, high].
    
    Optimization: Use BST property to prune branches.
    
    Time: O(n) worst case, but typically better due to pruning
    
    >>> root = BSTNode(10)
    >>> root.left, root.right = BSTNode(5), BSTNode(15)
    >>> root.left.left, root.left.right = BSTNode(3), BSTNode(7)
    >>> range_sum_bst(root, 7, 15)  # 7 + 10 + 15 = 32
    32
    """
    if not root:
        return 0
    
    total = 0
    
    # Include current node if in range
    if low <= root.val <= high:
        total += root.val
    
    # Only go left if there might be values in range
    if root.val > low:
        total += range_sum_bst(root.left, low, high)
    
    # Only go right if there might be values in range
    if root.val < high:
        total += range_sum_bst(root.right, low, high)
    
    return total

BST Iterator (LeetCode 173)

class BSTIterator:
    """
    Implement an iterator over a BST.
    next() returns next smallest, hasNext() checks if more elements exist.
    
    Approach: Controlled inorder traversal using stack.
    
    Time: O(1) amortized for next(), O(h) for hasNext()
    Space: O(h) for stack
    """
    
    def __init__(self, root: Optional[BSTNode]):
        self.stack = []
        self._push_left(root)
    
    def _push_left(self, node: Optional[BSTNode]) -> None:
        """Push all left children onto stack."""
        while node:
            self.stack.append(node)
            node = node.left
    
    def next(self) -> int:
        """Return next smallest element."""
        node = self.stack.pop()
        self._push_left(node.right)
        return node.val
    
    def hasNext(self) -> bool:
        """Return True if more elements exist."""
        return len(self.stack) > 0


# Usage example
# root = sorted_array_to_bst([1, 2, 3, 4, 5])
# iterator = BSTIterator(root)
# while iterator.hasNext():
#     print(iterator.next())  # Prints 1, 2, 3, 4, 5

Delete Node in BST (LeetCode 450)

def delete_node(root: Optional[BSTNode], key: int) -> Optional[BSTNode]:
    """
    Delete a node with given key from BST.
    
    Cases:
    1. Leaf: Just remove
    2. One child: Replace with child
    3. Two children: Replace with inorder successor
    
    Time: O(h)
    
    >>> root = sorted_array_to_bst([1, 2, 3, 4, 5])
    >>> root = delete_node(root, 3)
    >>> # Tree structure changes
    """
    if not root:
        return None
    
    if key < root.val:
        root.left = delete_node(root.left, key)
    elif key > root.val:
        root.right = delete_node(root.right, key)
    else:
        # Found node to delete
        
        # Case 1 & 2: No children or one child
        if not root.left:
            return root.right
        if not root.right:
            return root.left
        
        # Case 3: Two children
        # Find inorder successor (min in right subtree)
        successor = root.right
        while successor.left:
            successor = successor.left
        
        # Copy successor value to current node
        root.val = successor.val
        
        # Delete successor from right subtree
        root.right = delete_node(root.right, successor.val)
    
    return root

7. Step-by-Step Worked Example

Problem: Recover Binary Search Tree (LeetCode 99)

Problem: Two nodes in a BST are swapped by mistake. Recover the BST without changing its structure.

Example:

Swapped BST:        Correct BST:
      1                  3
     /                  /
    3        →         1
     \                  \
      2                  2

Nodes 1 and 3 are swapped

Approach: In inorder traversal of valid BST, values are strictly increasing. In swapped BST, we’ll find violations where current < previous.

def recover_tree(root: Optional[BSTNode]) -> None:
    """
    Recover BST where two nodes are swapped.
    
    Key insight: Inorder should be increasing.
    - Adjacent swap: One violation (prev > curr)
    - Non-adjacent swap: Two violations
    
    Time: O(n), Space: O(h)
    """
    first = second = prev = None
    
    def inorder(node: Optional[BSTNode]) -> None:
        nonlocal first, second, prev
        
        if not node:
            return
        
        inorder(node.left)
        
        # Check for violation
        if prev and prev.val > node.val:
            if not first:
                # First violation: prev is the first wrong node
                first = prev
            # Always update second (handles both adjacent and non-adjacent)
            second = node
        
        prev = node
        inorder(node.right)
    
    inorder(root)
    
    # Swap values of the two wrong nodes
    if first and second:
        first.val, second.val = second.val, first.val

Trace through example [3, 1, 2]:

Inorder traversal encounters: 3 → 1 → 2

Step 1: Visit 3
  prev = None, no comparison
  prev = node(3)

Step 2: Visit 1  
  prev.val(3) > node.val(1) → Violation!
  first = prev = node(3)
  second = node(1)
  prev = node(1)

Step 3: Visit 2
  prev.val(1) < node.val(2) → No violation
  prev = node(2)

Result: first = node(3), second = node(1)
Swap: first.val ↔ second.val → Tree becomes [1, 3, 2] 

Wait, that's wrong! Let me retrace...
Actually the tree is:
      1
     /
    3
     \
      2

Inorder: 3, 2, 1 (following left-root-right from root 1)
Actually let me reconsider the tree structure...

If tree is:    1        
              /
             3
              \
               2

Inorder visits: 3, 2, 1

prev=None, visit 3: no check, prev=3
prev=3, visit 2: 3>2 violation! first=3, second=2, prev=2
prev=2, visit 1: 2>1 violation! second=1
final: first=3, second=1

Swap values: 3↔1

Result tree:   3
              /
             1
              \
               2

Inorder: 1, 2, 3 ✓ Valid BST!

8. Common Mistakes

  1. Not handling duplicates

    # If duplicates allowed, specify where they go
    # Convention 1: Duplicates go right
    if val < node.val:
        node.left = insert(node.left, val)
    else:  # val >= node.val
        node.right = insert(node.right, val)
    
    # Convention 2: Don't allow duplicates
    # Convention 3: Use count in node
    
  2. Incorrect BST validation

    # Wrong: Only checking immediate children
    def is_valid_wrong(node):
        if not node:
            return True
        if node.left and node.left.val >= node.val:
            return False
        if node.right and node.right.val <= node.val:
            return False
        return is_valid_wrong(node.left) and is_valid_wrong(node.right)
    
    # This fails for:
    #     5
    #    / \
    #   1   6
    #      / \
    #     3   7   <- 3 < 5 but is in right subtree!
    
    # Correct: Pass bounds
    def is_valid_correct(node, min_val, max_val):
        if not node:
            return True
        if not (min_val < node.val < max_val):
            return False
        return (is_valid_correct(node.left, min_val, node.val) and
                is_valid_correct(node.right, node.val, max_val))
    
  3. Deletion edge cases

    # Remember to handle:
    # - Deleting root node
    # - Deleting node with two children
    # - Node not found (no-op)
    
  4. Confusing BST with Binary Tree

    # BST LCA uses values: O(h)
    # Binary Tree LCA needs full traversal: O(n)
    
    # BST: Can compare values and go left/right
    # Binary Tree: Must check both subtrees
    
  5. Iterator invalidation

    # Modifying BST while iterating causes issues
    # Either: Iterate first, then modify
    # Or: Create copy of elements to iterate
    

9. Variations & Optimizations

BST with Parent Pointers

class BSTNodeWithParent:
    """BST node with parent pointer for O(1) parent access."""
    def __init__(self, val: int):
        self.val = val
        self.left = None
        self.right = None
        self.parent = None  # Useful for successor without recursion
    
def find_successor_with_parent(node: 'BSTNodeWithParent') -> Optional['BSTNodeWithParent']:
    """Find successor using parent pointers."""
    if node.right:
        # Go to rightmost of right subtree
        current = node.right
        while current.left:
            current = current.left
        return current
    
    # Go up until we come from a left child
    parent = node.parent
    while parent and node == parent.right:
        node = parent
        parent = parent.parent
    return parent

Threaded BST

class ThreadedBSTNode:
    """
    Threaded BST: null pointers replaced with inorder successor/predecessor.
    Enables O(1) successor finding without stack/recursion.
    """
    def __init__(self, val: int):
        self.val = val
        self.left = None
        self.right = None
        self.is_threaded = False  # True if right points to successor

Floor and Ceiling

def floor_bst(root: Optional[BSTNode], key: int) -> Optional[int]:
    """
    Find largest value <= key.
    
    >>> root = sorted_array_to_bst([1, 3, 5, 7, 9])
    >>> floor_bst(root, 6)  # Largest <= 6 is 5
    5
    """
    floor = None
    current = root
    
    while current:
        if current.val == key:
            return current.val
        elif current.val < key:
            floor = current.val  # Potential floor
            current = current.right
        else:
            current = current.left
    
    return floor


def ceiling_bst(root: Optional[BSTNode], key: int) -> Optional[int]:
    """
    Find smallest value >= key.
    
    >>> root = sorted_array_to_bst([1, 3, 5, 7, 9])
    >>> ceiling_bst(root, 6)  # Smallest >= 6 is 7
    7
    """
    ceiling = None
    current = root
    
    while current:
        if current.val == key:
            return current.val
        elif current.val > key:
            ceiling = current.val  # Potential ceiling
            current = current.left
        else:
            current = current.right
    
    return ceiling

Two Sum in BST

def find_target_bst(root: Optional[BSTNode], k: int) -> bool:
    """
    Find if two nodes sum to k (LeetCode 653).
    
    Approach: Use two iterators - one forward (smallest up), one backward (largest down).
    
    Time: O(n), Space: O(h)
    """
    # Forward iterator (ascending)
    stack_asc = []
    node = root
    while node:
        stack_asc.append(node)
        node = node.left
    
    # Backward iterator (descending)
    stack_desc = []
    node = root
    while node:
        stack_desc.append(node)
        node = node.right
    
    # Two pointers
    while stack_asc and stack_desc:
        low = stack_asc[-1]
        high = stack_desc[-1]
        
        if low == high:  # Same node
            break
        
        total = low.val + high.val
        
        if total == k:
            return True
        elif total < k:
            # Need larger low, advance forward iterator
            node = stack_asc.pop()
            node = node.right
            while node:
                stack_asc.append(node)
                node = node.left
        else:
            # Need smaller high, advance backward iterator
            node = stack_desc.pop()
            node = node.left
            while node:
                stack_desc.append(node)
                node = node.right
    
    return False

10. Interview Tips

What Interviewers Look For

  1. Understanding BST property: Can you explain why inorder gives sorted order?
  2. Handling edge cases: Empty tree, single node, skewed trees
  3. Choosing approach: Recursive vs iterative based on constraints
  4. Optimization: Can you use BST property to prune search space?

Common Follow-up Questions

Q: What if tree is very deep (10^6 nodes in line)?
A: Use iterative approach to avoid stack overflow.

Q: How would you handle duplicates?
A: Options: 1) Count in node, 2) Duplicates go left/right, 3) No duplicates allowed

Q: How to make operations O(log n) guaranteed?
A: Use balanced BST (AVL, Red-Black) - covered in next file.

Quick Pattern Recognition

See "sorted" + tree? → BST
See "kth smallest/largest"? → Inorder traversal
See "validate BST"? → Range validation or inorder increasing check
See "LCA in BST"? → Use BST property (compare values)
See "construct BST"? → Sorted array → balanced, preorder → reconstruct

PatternDescriptionExample Problems
Inorder = SortedUse for kth element, range queriesKth Smallest, Range Sum
Range ValidationPass min/max bounds downValidate BST
BST NavigationUse value comparison to go left/rightLCA, Search, Successor
Two IteratorsTraverse from both endsTwo Sum BST, Merge Two BSTs

12. References

  • CLRS: Chapter 12 (Binary Search Trees)
  • LeetCode: BST tag (50+ problems)
  • Grokking: Tree DFS, BST patterns
  • Visualization: VisuAlgo BST

13. Practice Problems

Easy

#ProblemKey ConceptsLeetCode
1Search in BSTBasic BST search700
2Range Sum of BSTPruning with BST property938
3Minimum Absolute Difference in BSTInorder gives sorted530
4Two Sum IV - BSTTwo pointers on BST653
5Convert Sorted Array to BSTDivide and conquer108
6Lowest Common Ancestor of BSTUse BST property235

Medium

#ProblemKey ConceptsLeetCode
7Validate BSTRange validation98
8Kth Smallest Element in BSTInorder traversal230
9Delete Node in BSTThree cases450
10Insert into BSTBasic BST insert701
11BST IteratorControlled inorder173
12Construct BST from PreorderStack-based construction1008
13Convert BST to Greater TreeReverse inorder538
14Inorder Successor in BSTSuccessor finding285
15Trim a BSTRange-based pruning669
16Balance a BSTInorder + rebuild1382
17All Elements in Two BSTsMerge two sorted1305
18Unique BSTsCatalan numbers96

Hard

#ProblemKey ConceptsLeetCode
19Recover BSTFind two swapped nodes99
20Count of Smaller Numbers After SelfBST with count315
21Serialize and Deserialize BSTPreorder encoding449
22Maximum Sum BST in Binary TreeValidate + sum1373

14. Key Takeaways

  1. BST = Sorted Data Structure: Inorder traversal is always sorted

  2. Use BST property to prune: Don’t explore subtrees that can’t contain answer

  3. Average O(log n), Worst O(n): Skewed trees degrade to linked list

  4. Three deletion cases: Leaf, one child, two children (replace with successor)

  5. Successor/Predecessor: Key operations, can be done in O(h)

  6. When to use BST over hash table:

    • Need ordered iteration
    • Need range queries
    • Need min/max efficiently
    • Memory is constrained (no load factor overhead)

15. Spaced Repetition Prompts

  1. Q: What is the BST property? A: For every node, all values in left subtree are less than node value, all values in right subtree are greater than node value.

  2. Q: How do you find kth smallest in BST? A: Inorder traversal visits nodes in sorted order. Stop after visiting k nodes. Time: O(h + k).

  3. Q: How do you validate a BST? A: Pass min/max bounds recursively. Left child gets (min, node.val), right child gets (node.val, max). Check node.val is within bounds.

  4. Q: How do you find LCA in BST? A: Start from root. If both values < current, go left. If both > current, go right. Otherwise, current is LCA.

  5. Q: What are the three cases for BST deletion? A: 1) Leaf: just remove. 2) One child: replace with child. 3) Two children: replace with inorder successor (min in right subtree), then delete successor.