Module 5.2

Binary Search Trees

Think of a BST like a perfectly organized phone book where you can find any name in seconds πŸ“–β€”smaller names go left, larger names go right! This simple rule enables lightning-fast O(log n) search, insert, and delete operations. Master BST validation, successor/predecessor, and ace those interview questions!

45 min read
Intermediate
High Frequency
What You'll Learn
  • BST property
  • Search, Insert, Delete
  • BST validation
  • Kth smallest/largest
  • LCA in BST
Contents
01

BST Property

Binary Search Tree

A binary tree where for every node: all values in the left subtree are smaller, and all values in the right subtree are larger. This enables O(log n) operations.

Think of it Like a Phone Book!

Imagine you're looking for "Johnson" in a phone book. You don't start from page 1 - you open somewhere in the middle. If you see "Miller", you know Johnson comes BEFORE, so you flip backward. If you see "Davis", Johnson comes AFTER, so you flip forward.

A BST works exactly the same way! At each node, you make ONE decision: go left (smaller) or go right (larger). This eliminates half the remaining nodes with each step - that's why it's so fast!

Real-world uses: Database indexes, file system directories, autocomplete suggestions, and spell checkers all use BST-like structures for fast lookups.

// BST Node Structure
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    
    TreeNode(int val) {
        this.val = val;
    }
}

// Example valid BST:
//        8
//       / \
//      3   10
//     / \    \
//    1   6    14
//       / \   /
//      4   7 13

// Key Property: Inorder traversal gives sorted order
// Inorder: 1, 3, 4, 6, 7, 8, 10, 13, 14
Understanding the BST Property - Step by Step

Let's break down what makes a valid BST. This is crucial because many interview problems test your understanding of this property!

1
The Golden Rule

For ANY node in the tree:

Left Child < Parent < Right Child

This seems simple, but there's a catch...

2
It's Not Just Immediate Children!

The rule applies to the ENTIRE subtree, not just direct children.

Example: Node 8's left subtree contains [1, 3, 4, 6, 7]
ALL of these must be < 8!
3
Common Mistake

Beginners often check only parent-child relationships!

Root=5, Left=3, 3's Right=7
INVALID: 7 > 5 but in left subtree!
4
Inorder = Sorted

Traverse in Left β†’ Root β†’ Right order and you get a sorted sequence!

This is the easiest way to verify a valid BST!
5
No Duplicates (Usually)

Standard BSTs don't allow duplicate values.

If you need duplicates: all go left OR all go right (be consistent!)
Quick Memory Trick: "Everything left is smaller, everything right is bigger" - applies to ALL descendants, not just children!
Visual: The BST Property Left < Root < Right
BST Property: Left < Root < Right
8 ← Root 3 10 3 < 8 < 10 βœ“ 1 6 14 4 7 13
BST Rules
  • ALL left subtree values < root
  • ALL right subtree values > root
  • Applies recursively to all subtrees
Color Legend
Left Subtree Right Subtree Middle Nodes
Operation Average Worst (Skewed)
Search O(log n) O(n)
Insert O(log n) O(n)
Delete O(log n) O(n)
Min/Max O(log n) O(n)

Practice Questions

Problem: Given a BST root and a target value, return true if the value exists in the tree.

// Input: root = [8,3,10,1,6,null,14], target = 6
// Output: true

// Input: root = [8,3,10,1,6,null,14], target = 12
// Output: false
View Solution
boolean exists(TreeNode root, int target) {
    if (root == null) return false;
    if (root.val == target) return true;
    
    if (target < root.val) {
        return exists(root.left, target);
    } else {
        return exists(root.right, target);
    }
}

Problem: Count how many nodes have values in the range [low, high] inclusive.

// Input: root = [10,5,15,3,7,null,18], low = 7, high = 15
// Output: 3 (nodes: 7, 10, 15)
View Solution
int countInRange(TreeNode root, int low, int high) {
    if (root == null) return 0;
    
    int count = 0;
    if (root.val >= low && root.val <= high) count = 1;
    
    if (root.val > low) count += countInRange(root.left, low, high);
    if (root.val < high) count += countInRange(root.right, low, high);
    
    return count;
}

Problem: Find the minimum and maximum values in a BST without traversing the entire tree.

// Input: root = [8,3,10,1,6,null,14]
// Output: min = 1, max = 14
View Solution
int findMin(TreeNode root) {
    // Leftmost node is minimum
    while (root.left != null) {
        root = root.left;
    }
    return root.val;
}

int findMax(TreeNode root) {
    // Rightmost node is maximum
    while (root.right != null) {
        root = root.right;
    }
    return root.val;
}

Problem: Given a binary tree, check if it's a valid BST using inorder traversal property.

// Input: root = [5,3,7,2,4,6,8]
// Output: true (inorder: 2,3,4,5,6,7,8 is sorted)
View Solution
Integer prev = null;

boolean isValidBST(TreeNode root) {
    if (root == null) return true;
    
    // Check left subtree
    if (!isValidBST(root.left)) return false;
    
    // Check current node with previous
    if (prev != null && root.val <= prev) return false;
    prev = root.val;
    
    // Check right subtree
    return isValidBST(root.right);
}
03

Insert Operation

Inserting into a BST is beautifully simple: search for where the value SHOULD be, and when you hit an empty spot (null), that's where it goes! New nodes are always added as leaf nodes - we never insert in the middle of the tree.

Critical Concept: Insertion Order Matters!

The order you insert values determines the tree's shape. Insert [4,2,6,1,3,5,7] and you get a perfectly balanced tree. Insert [1,2,3,4,5,6,7] in sorted order and you get a "skewed" tree that looks like a linked list!

A skewed tree has O(n) operations instead of O(log n). This is why self-balancing trees (AVL, Red-Black) exist - they automatically rebalance after insertions.

Recursive Insert O(log n) average
TreeNode insert(TreeNode root, int val) {
    // Base case: found the spot - create new node
    if (root == null) {
        return new TreeNode(val);
    }
    
    // Decide direction based on BST property
    if (val < root.val) {
        root.left = insert(root.left, val);    // Go left
    } else if (val > root.val) {
        root.right = insert(root.right, val);  // Go right
    }
    // If equal, ignore duplicates (standard BST behavior)
    
    return root;  // Return unchanged root to maintain tree structure
}
Base Case

root == null β†’ Create new node
This is where insertion happens!

Go Left

When val < root.val
Assign result to root.left

Go Right

When val > root.val
Assign result to root.right

The recursive insert is elegant and intuitive. It follows the same path as search: compare, go left or right, repeat. The magic is in the base case - when we hit null, we've found where the new value belongs! We create a new node and return it. The parent's assignment (root.left = insert(...)) connects the new node to the tree. Always return root at the end - this is what makes the recursion work correctly!

Iterative Insert O(log n) average O(1) space
TreeNode insertIterative(TreeNode root, int val) {
    TreeNode newNode = new TreeNode(val);
    
    // Special case: empty tree
    if (root == null) {
        return newNode;
    }
    
    TreeNode curr = root;
    TreeNode parent = null;
    
    // Find the correct position
    while (curr != null) {
        parent = curr;  // Track parent before moving
        if (val < curr.val) {
            curr = curr.left;
        } else {
            curr = curr.right;
        }
    }
    
    // Attach new node to parent
    if (val < parent.val) {
        parent.left = newNode;
    } else {
        parent.right = newNode;
    }
    
    return root;
}
Track Parent

Keep parent pointer
We need it to attach the new node

Find Position

Loop until curr == null
This is where new node belongs

Attach Node

Compare with parent's value
Attach to left or right accordingly

The iterative approach requires more code but uses O(1) space - no recursion stack! The key difference: we need to manually track the parent pointer because once curr becomes null, we lose access to where we were. The loop finds the correct spot, then we make one final comparison with the parent to decide if the new node goes left or right. This is a common interview follow-up: "Can you do it without recursion?"

Build BST from Array O(n log n) average
TreeNode buildBST(int[] nums) {
    TreeNode root = null;
    for (int num : nums) {
        root = insert(root, num);  // Insert each element
    }
    return root;
}
Start Empty

Initialize root = null
First insert creates the root node

Loop & Insert

Insert each array element one by one
Tree grows with each insertion

Warning: Input order determines tree shape! Sorted input β†’ skewed tree (O(n) operations)

This utility function builds a complete BST from an array by inserting elements one at a time. Critical insight: the array's order determines the tree's shape! Insert [4,2,6,1,3,5,7] and you get a balanced tree. Insert [1,2,3,4,5,6,7] (sorted) and you get a "linked list" - each node only has a right child. For interviews, remember: if you need a balanced BST from sorted data, use the "build from sorted array" technique (divide and conquer) instead!

How Insert Works - Complete Breakdown

Insertion follows the exact same path as search, but instead of returning "not found", we create a new node at the empty spot. It's beautifully simple!

1 Empty tree?

The new node becomes the root. This is your base case - when root == null, return a new node!

2 Compare & Decide

If new value < current β†’ go left. If new value > current β†’ go right.

3 Recurse Down

Call recursively: root.left = insert(root.left, val). The returned value updates the child pointer.

4 Hit null = Found the spot!

When we reach an empty spot (null), create and return the new node. The parent's assignment connects it to the tree automatically!

5 Return the root

Each recursive call returns the (unchanged) root of its subtree. This "bubbles up" to return the original root with the new node attached.

Why return root?

The pattern root.left = insert(root.left, val); return root; is elegant:

When hitting null β†’ Return new node
All other nodes β†’ Return themselves unchanged
Visual: Inserting Value 5
BEFORE
8 3 10 1 6
5 < 8
↙ left
5 > 3
β†˜ right
5 < 6
↙ left
null!
INSERT
AFTER
8 3 10 1 6 5 ← NEW!
New nodes always become LEAF nodes!

Practice Questions

Problem: Given an array, build a BST by inserting values in order. Return the root.

// Input: [8, 3, 10, 1, 6, 14]
// Output: BST with root 8
View Solution
TreeNode buildBST(int[] values) {
    TreeNode root = null;
    for (int val : values) {
        root = insert(root, val);
    }
    return root;
}

TreeNode insert(TreeNode root, int val) {
    if (root == null) return new TreeNode(val);
    if (val < root.val) root.left = insert(root.left, val);
    else root.right = insert(root.right, val);
    return root;
}

Problem: Given sorted array [1,2,3,4,5,6,7], find an insertion order that creates a balanced BST.

View Solution
// Insert middle element first, then recursively do same for left and right halves
// Order: 4, 2, 6, 1, 3, 5, 7

//         4
//        / \
//       2   6
//      / \ / \
//     1  3 5  7

void getBalancedOrder(int[] sorted, int left, int right, List order) {
    if (left > right) return;
    int mid = left + (right - left) / 2;
    order.add(sorted[mid]);
    getBalancedOrder(sorted, left, mid - 1, order);
    getBalancedOrder(sorted, mid + 1, right, order);
}

Problem: Insert a value into BST and return the depth (level) where it was inserted.

// Input: root = [8,3,10], val = 6
// Output: 2 (inserted at depth 2: 8 -> 3 -> 6)
View Solution
int insertAndGetDepth(TreeNode root, int val) {
    if (root == null) return 0;  // Empty tree, root level
    
    int depth = 0;
    TreeNode curr = root;
    
    while (true) {
        depth++;
        if (val < curr.val) {
            if (curr.left == null) {
                curr.left = new TreeNode(val);
                return depth;
            }
            curr = curr.left;
        } else {
            if (curr.right == null) {
                curr.right = new TreeNode(val);
                return depth;
            }
            curr = curr.right;
        }
    }
}

Problem: Before inserting, check if the value already exists in the BST.

// Input: root = [8,3,10,1,6], val = 6
// Output: true (6 already exists)
View Solution
boolean wouldBeDuplicate(TreeNode root, int val) {
    while (root != null) {
        if (val == root.val) return true;
        if (val < root.val) {
            root = root.left;
        } else {
            root = root.right;
        }
    }
    return false;
}

// Safe insert that prevents duplicates
TreeNode safeInsert(TreeNode root, int val) {
    if (wouldBeDuplicate(root, val)) {
        return root;  // Don't insert duplicate
    }
    return insert(root, val);
}
04

Delete Operation

Deletion is the trickiest BST operation because removing a node might "break" the tree structure. The complexity depends on how many children the node has: no children is easy, one child is straightforward, but two children requires a clever trick using the inorder successor.

Why Deletion is Hard (The Two-Children Problem)

Imagine deleting a node that has both a left child and a right child. You can't just "remove" it - both subtrees would become disconnected! You need to find a replacement value that:

  • Is larger than everything in the left subtree (to maintain BST property with left)
  • Is smaller than everything in the right subtree (to maintain BST property with right)

The inorder successor (smallest value in right subtree) satisfies both conditions perfectly! It's the "next" value in sorted order.

Three Cases:
1. Leaf node: Simply remove it
2. One child: Replace with child
3. Two children: Replace with inorder successor (or predecessor)
Delete - Main Function O(log n) average
TreeNode delete(TreeNode root, int key) {
    if (root == null) return null;
    
    // Search for node to delete
    if (key < root.val) {
        root.left = delete(root.left, key);
    } else if (key > root.val) {
        root.right = delete(root.right, key);
    } else {
        // Found the node to delete - handle 3 cases
        
        // Case 1: Leaf node (no children)
        if (root.left == null && root.right == null) {
            return null;
        }
        
        // Case 2: One child only
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        
        // Case 3: Two children - use inorder successor
        TreeNode successor = findMin(root.right);
        root.val = successor.val;
        root.right = delete(root.right, successor.val);
    }
    
    return root;
}
Case 1: Leaf

No children β†’ return null
Parent's pointer becomes null

Case 2: One Child

Return the single child
Grandchild replaces deleted node

Case 3: Two Children

Copy successor's value
Then delete the successor

The delete function first searches for the node (like regular BST search), then handles three cases based on children count. Case 3 is the tricky one: we find the inorder successor (smallest in right subtree), copy its value to the current node, then recursively delete the successor. This works because the successor has at most one child (no left child by definition), so deleting it falls into Case 1 or 2!

Helper: Find Minimum O(h) time
TreeNode findMin(TreeNode node) {
    while (node.left != null) {
        node = node.left;
    }
    return node;
}
Keep going left until node.left == null. The leftmost node is always the smallest!

This helper is essential for delete - it finds the inorder successor. In a BST, the smallest value in any subtree is always at the leftmost position. We use this to find the "next" value in sorted order when deleting a node with two children. The successor is guaranteed to have no left child (otherwise it wouldn't be the minimum)!

Alternative: Using Predecessor Also valid!
TreeNode deleteWithPredecessor(TreeNode root, int key) {
    if (root == null) return null;
    
    if (key < root.val) {
        root.left = deleteWithPredecessor(root.left, key);
    } else if (key > root.val) {
        root.right = deleteWithPredecessor(root.right, key);
    } else {
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        
        // Find inorder predecessor (largest in left subtree)
        TreeNode predecessor = findMax(root.left);
        root.val = predecessor.val;
        root.left = deleteWithPredecessor(root.left, predecessor.val);
    }
    
    return root;
}
Predecessor vs Successor

Successor: smallest in right subtree
Predecessor: largest in left subtree

Which to Use?

Both work! Some prefer predecessor to avoid traversing right subtree twice.

Instead of the inorder successor, you can use the inorder predecessor (largest value in left subtree). The logic is symmetric: find the rightmost node in the left subtree, copy its value, then delete it. Interview tip: Mention this alternative to show deeper understanding. Both approaches maintain the BST property perfectly!

The Three Delete Cases - Deep Dive

Understanding WHY each case works is crucial for interviews. Deletion is tricky because we must maintain the BST property after removing a node!

1 Leaf Node (No Children)

The simplest case! The node has no children, so removing it won't break any connections.

How it works:

Return null to parent. Parent's root.left = delete(...) sets child to null, removing the node.

2 One Child

Node has exactly one child. We can "bypass" the node by connecting parent directly to grandchild.

How it works:

Return the single child to parent. Grandchild becomes new child. BST property preserved since entire subtree was already valid!

3 Two Children (Tricky!)

Can't just remove - both subtrees would disconnect! Need a replacement value.

3-Step Solution:
  1. Find inorder successor
  2. Copy its value to node
  3. Delete the successor
Why Does Inorder Successor Work?

The inorder successor is the SMALLEST value that is LARGER than the deleted node. When we move it up:

Left subtree stays valid

Everything was smaller than original, successor is larger than original β†’ all still smaller than successor

Right subtree stays valid

Successor was the smallest in right subtree β†’ all others in right are larger than successor

Visual: The Three Delete Cases
CASE 1 Leaf Node

No children - just remove it!

Delete(1) 3 1 βœ• delete 6
Result 3 6
Just remove it!
CASE 2 One Child

Replace node with its only child

Delete(10) 8 3 10 14
Result 8 3 14 ↑ moved up
Replace with child!
CASE 3 Two Children

Find inorder successor, swap & delete

Delete(3) 8 3 10 1 6 4
Result 8 4 ↑ successor 10 1 6
Copy successor, delete it!

Practice Questions

Problem: Remove all nodes with values outside [min, max] range. Return new root.

// Input: root = [10,5,15,3,7,13,18], min = 5, max = 13
// Output: BST with only nodes 5, 7, 10, 13
View Solution
TreeNode trimBST(TreeNode root, int min, int max) {
    if (root == null) return null;
    
    // If current value is too small, trim from right subtree
    if (root.val < min) return trimBST(root.right, min, max);
    
    // If current value is too large, trim from left subtree
    if (root.val > max) return trimBST(root.left, min, max);
    
    // Current is in range, trim both subtrees
    root.left = trimBST(root.left, min, max);
    root.right = trimBST(root.right, min, max);
    
    return root;
}

Problem: Implement BST deletion using inorder predecessor instead of successor for the two-children case.

View Solution
TreeNode deleteWithPredecessor(TreeNode root, int key) {
    if (root == null) return null;
    
    if (key < root.val) {
        root.left = deleteWithPredecessor(root.left, key);
    } else if (key > root.val) {
        root.right = deleteWithPredecessor(root.right, key);
    } else {
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        
        // Find predecessor (rightmost in left subtree)
        TreeNode pred = root.left;
        while (pred.right != null) pred = pred.right;
        
        root.val = pred.val;
        root.left = deleteWithPredecessor(root.left, pred.val);
    }
    return root;
}

Problem: Delete a specific leaf node (node with no children) from BST.

// Input: root = [8,3,10,1,6], key = 1
// Output: BST with node 1 removed
View Solution
TreeNode deleteLeaf(TreeNode root, int key) {
    if (root == null) return null;
    
    if (key < root.val) {
        root.left = deleteLeaf(root.left, key);
    } else if (key > root.val) {
        root.right = deleteLeaf(root.right, key);
    } else {
        // Found the node
        // Only delete if it's a leaf
        if (root.left == null && root.right == null) {
            return null;  // Delete leaf
        }
        // Not a leaf, don't delete
    }
    return root;
}

Problem: Remove all leaf nodes from the BST in one pass.

// Input: root = [8,3,10,1,6,null,14]
// Output: BST with only nodes 8, 3, 10 (leaves 1,6,14 removed)
View Solution
TreeNode removeAllLeaves(TreeNode root) {
    if (root == null) return null;
    
    // If it's a leaf, remove it
    if (root.left == null && root.right == null) {
        return null;
    }
    
    // Recursively remove leaves from subtrees
    root.left = removeAllLeaves(root.left);
    root.right = removeAllLeaves(root.right);
    
    return root;
}
05

Validate BST

Validating a BST is a classic interview question (LeetCode #98). The naive approach of checking "left child < parent < right child" at each node is WRONG - it misses cases where a node violates the property with an ancestor, not its parent.

The Classic Mistake (Don't Fall For This!)

Many beginners write code that only checks: node.left.val < node.val && node.val < node.right.val

❌ This FAILS!
5 1 6 3 7

Why 3 is WRONG here:

  • Local check passes: 3 < 6 (parent check OK)
  • Global check fails: 3 < 5 but 3 is in right subtree of 5!
  • Rule: All nodes in right subtree must be > 5
The Fix: Each node must be within a RANGE defined by all its ancestors, not just its parent. Pass min/max bounds down the tree!
Method 1: Min/Max Bounds ⭐ Recommended! O(n) time
boolean isValidBST(TreeNode root) {
    return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}

boolean validate(TreeNode node, long min, long max) {
    if (node == null) return true;
    
    // Node must be within valid range
    if (node.val <= min || node.val >= max) {
        return false;
    }
    
    // Left subtree: max becomes current value
    // Right subtree: min becomes current value
    return validate(node.left, min, node.val) && 
           validate(node.right, node.val, max);
}
Range Check

Each node must be:
min < node.val < max

Going Left

Update max = node.val
All left descendants < parent

Going Right

Update min = node.val
All right descendants > parent

This is the most intuitive approach! Instead of just checking parent-child relationships, we pass down a valid range that tightens as we go deeper. When going left, the current node becomes the new upper bound. When going right, it becomes the new lower bound. We use Long to handle edge cases where node values are Integer.MIN_VALUE or Integer.MAX_VALUE.

Method 2: Inorder Traversal O(n) time, O(h) space
boolean isValidBSTInorder(TreeNode root) {
    Stack stack = new Stack<>();
    TreeNode prev = null;
    TreeNode curr = root;
    
    while (curr != null || !stack.isEmpty()) {
        // Go all the way left
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;
        }
        
        curr = stack.pop();
        
        // Check if current > previous (sorted order)
        if (prev != null && curr.val <= prev.val) {
            return false;
        }
        
        prev = curr;
        curr = curr.right;
    }
    
    return true;
}
Key Insight

Inorder traversal of a valid BST produces sorted order. If any value ≀ previous, it's invalid!

Validation Logic

Track prev node. Each curr.val must be strictly greater than prev.val.

This method exploits the fact that inorder traversal = sorted order for a valid BST. We do an iterative inorder traversal and check if each value is greater than the previous. The moment we find curr.val <= prev.val, we know it's invalid. This approach is elegant and shows understanding of BST properties!

Method 3: Recursive Inorder Cleanest code
TreeNode prev = null;  // Instance variable

boolean isValidBSTRecursive(TreeNode root) {
    if (root == null) return true;
    
    // Check left subtree first
    if (!isValidBSTRecursive(root.left)) return false;
    
    // Check current node against previous
    if (prev != null && root.val <= prev.val) return false;
    prev = root;
    
    // Check right subtree
    return isValidBSTRecursive(root.right);
}
Why Instance Variable?

prev persists across recursive calls to track the last visited node in inorder.

Caveat

Must reset prev = null before each validation call if reusing the method!

Same logic as Method 2, but recursive and cleaner. The instance variable prev tracks the last node visited in inorder sequence. We visit left subtree, check current node, then visit right subtree - classic inorder pattern. Some consider this less elegant due to the instance variable, but the code is very readable!

Validation Methods - Which to Use?

All three methods are valid, but they have different trade-offs:

MethodHow It WorksPros/Cons
Min/Max Bounds Pass [min, max] range to each node. For left child, update max to parent's value. For right child, update min to parent's value. Most intuitive! Easy to explain in interviews. Uses Long.MIN/MAX_VALUE to handle Integer.MIN_VALUE edge case.
Inorder Check Inorder traversal of valid BST is sorted. Track previous value and verify each node is greater. Alternative perspective. Uses the property that inorder = sorted. Can use Morris traversal for O(1) space.
Recursive Inorder Same as above but recursive with global/instance variable to track previous node. Cleanest code. But uses instance variable which some consider less elegant.
Interview Recommendation: Use Method 1 (Min/Max Bounds). It's the most intuitive to explain, shows clear understanding of the BST property, and handles edge cases cleanly. If asked for follow-up, mention that inorder traversal is an alternative approach.

Practice Questions

Problem: Identify why this tree is NOT a valid BST:

//       10
//      /  \
//     5    15
//    / \   / \
//   3   12 12  20
View Solution

Two issues:

  1. Node 12 in left subtree of 10 violates BST property (12 > 10 but in left subtree)
  2. Duplicate value 12 appears twice (BSTs typically don't allow duplicates, or all duplicates go one side)

The BST property requires ALL nodes in left subtree < root, not just immediate children!

Problem: Given a BST and a target sum, return true if two nodes exist whose values add up to target.

// Input: root = [5,3,6,2,4,null,7], target = 9
// Output: true (2 + 7 = 9)
View Solution
// Use inorder traversal + two pointers (like two-sum in sorted array)
boolean findTarget(TreeNode root, int target) {
    List sorted = new ArrayList<>();
    inorder(root, sorted);
    
    int left = 0, right = sorted.size() - 1;
    while (left < right) {
        int sum = sorted.get(left) + sorted.get(right);
        if (sum == target) return true;
        if (sum < target) left++;
        else right--;
    }
    return false;
}

void inorder(TreeNode node, List list) {
    if (node == null) return;
    inorder(node.left, list);
    list.add(node.val);
    inorder(node.right, list);
}

Problem: Check if all leaf nodes in the BST are at the same depth.

// Input: root = [4,2,6,1,3,5,7] (perfect BST)
// Output: true (all leaves at level 2)
View Solution
int leafLevel = -1;

boolean allLeavesAtSameLevel(TreeNode root) {
    leafLevel = -1;
    return checkLeaves(root, 0);
}

boolean checkLeaves(TreeNode node, int level) {
    if (node == null) return true;
    
    // If leaf node
    if (node.left == null && node.right == null) {
        if (leafLevel == -1) {
            leafLevel = level;  // First leaf found
            return true;
        }
        return level == leafLevel;  // Compare with first leaf
    }
    
    return checkLeaves(node.left, level + 1) && 
           checkLeaves(node.right, level + 1);
}

Problem: Given a binary tree (not necessarily BST), find the largest subtree that is a valid BST.

// Input: root = [10,5,15,1,8,null,7]
// Node 15's right child 7 violates BST
// Output: 3 (subtree rooted at 5 with nodes 1,5,8)
View Solution
int maxBSTSize = 0;

int largestBSTSubtree(TreeNode root) {
    maxBSTSize = 0;
    traverse(root);
    return maxBSTSize;
}

// Returns [size, min, max] if valid BST, null otherwise
int[] traverse(TreeNode node) {
    if (node == null) return new int[]{0, Integer.MAX_VALUE, Integer.MIN_VALUE};
    
    int[] left = traverse(node.left);
    int[] right = traverse(node.right);
    
    // Check if current subtree is valid BST
    if (left != null && right != null &&
        node.val > left[2] && node.val < right[1]) {
        int size = left[0] + right[0] + 1;
        maxBSTSize = Math.max(maxBSTSize, size);
        return new int[]{size, 
                         Math.min(node.val, left[1]), 
                         Math.max(node.val, right[2])};
    }
    
    return null;  // Not a valid BST
}
06

Common BST Problems

These problems appear frequently in coding interviews! They all leverage the BST property in clever ways. Master these patterns and you'll be ready for most BST questions.

The Secret to BST Problems

Almost every BST problem can be solved by exploiting ONE of these three super-powers. Identify which one applies and you're 80% done!

1 Inorder = Sorted

In a BST, inorder traversal (Left β†’ Root β†’ Right) visits nodes in sorted order!

Use when you need:
  • Kth smallest/largest element
  • Range sum queries
  • Check if BST is valid
  • Convert BST to sorted list
2 Search Path

Use BST property to navigate: smaller β†’ left, larger β†’ right. No need to check both sides!

Use when you need:
  • Lowest Common Ancestor
  • Inorder successor/predecessor
  • Search, Insert, Delete
  • Floor/Ceiling of a value
3 Divide & Conquer

Break into: left subtree + root + right subtree. Combine results recursively!

Use when you need:
  • Build BST from sorted array
  • Validate BST structure
  • Serialize/Deserialize BST
  • Balance a BST
Quick Decision Guide:

Need sorted order? β†’ Use Inorder | Need to find/locate something? β†’ Use Search Path | Need to build/validate? β†’ Use Divide & Conquer

Kth Smallest Element

What is Kth Smallest?

Given a BST and a number k, find the kth smallest value in the tree. This is a classic interview question (LeetCode #230).

Example: k = 3
5 3 7 1 4
Answer: 3

Why Inorder Traversal?

In a BST, inorder traversal (Left β†’ Root β†’ Right) visits nodes in sorted order!

Inorder of above tree:
1 β†’ 2nd β†’ 3rd β†’ 4 β†’ 5

Algorithm: Do inorder traversal, count nodes. When count equals k, that's your answer!

Time Complexity: O(H + k) where H is height. Best case O(k) when tree is balanced. Worst case O(n) for skewed tree or large k.
Kth Smallest Element O(H + k) time
int kthSmallest(TreeNode root, int k) {
    Stack stack = new Stack<>();
    TreeNode curr = root;
    int count = 0;
    
    while (curr != null || !stack.isEmpty()) {
        // Go all the way left first
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;
        }
        
        // Pop and process
        curr = stack.pop();
        count++;
        
        // Found kth smallest!
        if (count == k) {
            return curr.val;
        }
        
        // Move to right subtree
        curr = curr.right;
    }
    
    return -1;  // k is larger than tree size
}
Key Insight

Inorder = sorted order!
Just count to k during traversal

Go Left First

Push all left nodes to stack
Reaches smallest element first

Early Exit

Stop at count == k
No need to traverse entire tree!

We use iterative inorder traversal which visits nodes in sorted order. Count each visited node, and when count reaches k, we've found our answer! The key optimization: we can exit early once we find the kth element - no need to traverse the entire tree. Time complexity is O(H + k) where H is height to reach the leftmost node, then k more steps.

Kth Largest Element Reverse Inorder
int kthLargest(TreeNode root, int k) {
    Stack stack = new Stack<>();
    TreeNode curr = root;
    int count = 0;
    
    while (curr != null || !stack.isEmpty()) {
        // Go all the way RIGHT first (reverse!)
        while (curr != null) {
            stack.push(curr);
            curr = curr.right;
        }
        
        curr = stack.pop();
        count++;
        
        if (count == k) {
            return curr.val;
        }
        
        // Move to LEFT subtree (reverse!)
        curr = curr.left;
    }
    
    return -1;
}
Just Reverse It!

Inorder: Left β†’ Root β†’ Right (ascending)
Reverse: Right β†’ Root β†’ Left (descending)

Interview Tip

Kth largest = (n-k+1)th smallest, but reverse inorder is more efficient!

For kth largest, we simply reverse the inorder traversal: go right first, then left. This gives us values in descending order. The 1st value visited is the largest, 2nd is second-largest, etc. Same logic, just swap left and right! This is more efficient than finding (n-k+1)th smallest because we don't need to know n.

Lowest Common Ancestor in BST

What is Lowest Common Ancestor (LCA)?

The Lowest Common Ancestor of two nodes p and q is the deepest node that has both p and q as descendants. Think of it as the "meeting point" when you trace paths from root to both nodes. (LeetCode #235)

Find LCA(2, 8)
6 LCA! 2 8 0 4 9
Answer: 6

Why BST Makes LCA Easy:

In a BST, we can find LCA in O(log n) without checking both subtrees!

The Algorithm:
  • If both p,q < root β†’ LCA is in left subtree
  • If both p,q > root β†’ LCA is in right subtree
  • Otherwise β†’ root IS the LCA (split point!)

Example: 2 < 6 and 8 > 6, so 6 is where they "split" β†’ LCA = 6

LCA - Iterative O(log n) time O(1) space
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    while (root != null) {
        if (p.val < root.val && q.val < root.val) {
            // Both nodes in left subtree
            root = root.left;
        } else if (p.val > root.val && q.val > root.val) {
            // Both nodes in right subtree
            root = root.right;
        } else {
            // Split point found - this is the LCA!
            return root;
        }
    }
    return null;
}
Both < Root

Both p,q are smaller
LCA must be in left subtree

Both > Root

Both p,q are larger
LCA must be in right subtree

Split Point!

p,q are on different sides
Current node IS the LCA

The BST property makes LCA incredibly efficient! We don't need to explore both subtrees like in a regular binary tree. If both p and q are smaller than root, their LCA must be in the left subtree. If both are larger, it's in the right subtree. The moment they "split" (one goes left, one goes right, or one equals root), we've found the LCA!

LCA - Recursive O(log n) time, O(h) space
TreeNode lcaRecursive(TreeNode root, TreeNode p, TreeNode q) {
    if (p.val < root.val && q.val < root.val) {
        return lcaRecursive(root.left, p, q);
    } else if (p.val > root.val && q.val > root.val) {
        return lcaRecursive(root.right, p, q);
    } else {
        return root;  // Split point = LCA
    }
}
Clean & Elegant

Same logic as iterative, just cleaner code. Base case: when nodes split, return current root.

Trade-off

Uses O(h) stack space vs O(1) for iterative. In interviews, mention both options!

The recursive version is shorter and cleaner - just 3 cases with direct recursion. Note how we don't need explicit null checks because we're guaranteed to find the LCA before hitting null (assuming p and q exist in the tree). Interview tip: Start with recursive for clarity, then offer to convert to iterative if space is a concern!

Inorder Successor/Predecessor

What is Inorder Successor & Predecessor?

If you arrange all BST values in sorted order, the successor is the "next" value and the predecessor is the "previous" value. These are crucial for BST deletion!

Find for node 4
5 3 7 1 4

Sorted order: 1, 3, 4, 5, 7

Predecessor
Value before 4 = 3
Successor
Value after 4 = 5

Use cases: BST deletion (two-children case), implementing iterators, finding next/prev in sorted data.

Inorder Successor O(h) time
TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
    TreeNode successor = null;
    
    while (root != null) {
        if (p.val < root.val) {
            // Root could be successor, save it
            successor = root;
            root = root.left;
        } else {
            // Successor must be in right subtree
            root = root.right;
        }
    }
    
    return successor;
}
When p.val < root.val

Root is larger than p β†’ potential successor!
Save it and go left to find smaller one

When p.val β‰₯ root.val

Root is too small or equal
Successor must be in right subtree

The inorder successor is the smallest value greater than p. We search the BST: when we go left, the current node is a potential successor (it's larger than p), so we save it. When we go right, the current node is too small. The last saved value when we exit is the smallest node that's still greater than p - exactly what we need!

Inorder Predecessor O(h) time
TreeNode inorderPredecessor(TreeNode root, TreeNode p) {
    TreeNode predecessor = null;
    
    while (root != null) {
        if (p.val > root.val) {
            // Root could be predecessor, save it
            predecessor = root;
            root = root.right;
        } else {
            // Predecessor must be in left subtree
            root = root.left;
        }
    }
    
    return predecessor;
}
When p.val > root.val

Root is smaller than p β†’ potential predecessor!
Save it and go right to find larger one

When p.val ≀ root.val

Root is too large or equal
Predecessor must be in left subtree

The inorder predecessor is the largest value smaller than p - exactly the opposite of successor! When we go right, the current node is smaller than p, so it's a potential predecessor (save it). When we go left, we're looking for larger values that are still smaller than p. The last saved value is the largest node that's still smaller than p. Notice the symmetry: swap left↔right and <↔> from successor!

Convert Sorted Array to BST

Convert Sorted Array to Balanced BST

Given a sorted array, build a height-balanced BST where the depth of subtrees differs by at most 1. This is LeetCode #108 and a beautiful example of divide-and-conquer!

Input: [1,2,3,4,5,6,7]
4 middle 2 6 1 3 5 7

The Key Insight:

Always pick the MIDDLE element as root!

Step by step:
  1. Middle of [1,2,3,4,5,6,7] = 4 β†’ root
  2. Left half [1,2,3] β†’ build left subtree
  3. Right half [5,6,7] β†’ build right subtree
  4. Recurse until arrays are empty

Why balanced? Middle splits array in half β†’ equal nodes on each side!

Sorted Array to Balanced BST O(n) time Divide & Conquer
TreeNode sortedArrayToBST(int[] nums) {
    return buildBST(nums, 0, nums.length - 1);
}

TreeNode buildBST(int[] nums, int left, int right) {
    // Base case: no elements in range
    if (left > right) return null;
    
    // Pick middle element as root (ensures balance!)
    int mid = left + (right - left) / 2;
    
    // Create node and build subtrees recursively
    TreeNode node = new TreeNode(nums[mid]);
    node.left = buildBST(nums, left, mid - 1);    // Left half
    node.right = buildBST(nums, mid + 1, right);  // Right half
    
    return node;
}
Pick Middle

mid = (left + right) / 2
Middle element becomes root

Left Half

[left, mid-1]
Build left subtree recursively

Right Half

[mid+1, right]
Build right subtree recursively

Why balanced? Picking the middle splits elements equally β†’ both subtrees have ~n/2 elements!

This is a beautiful divide-and-conquer algorithm! Since the array is sorted, the middle element is perfect as root: everything before it goes in the left subtree, everything after goes in the right subtree. By always picking the middle, we guarantee both subtrees have roughly equal nodes, creating a height-balanced BST. The recursion naturally handles all levels of the tree!

Why Sorted Array to BST Works - Step by Step

This is a beautiful example of divide-and-conquer that you should understand deeply. Let's trace through array [1, 2, 3, 4, 5, 6, 7]

1 Pick middle as root
1 2 3 4 5 6 7
Middle of [1-7] = 4 β†’ becomes root
2 Split into halves
Left half
1 2 3
Right half
5 6 7
3 Build left subtree
1 2 3
Middle of [1,2,3] = 2 β†’ left child of 4
4 Build right subtree
5 6 7
Middle of [5,6,7] = 6 β†’ right child of 4
Final Balanced BST
4 2 6 1 3 5 7
Why is it perfectly balanced?

By always picking the middle, each subtree gets exactly half the remaining elements. This guarantees O(log n) height. If we picked the first element each time, we'd get a skewed tree with O(n) height!

Practice Questions: Binary Search Trees

Given:

//       5
//      / \
//     3   7
//    / \   \
//   2   4   8

Task: Find the second smallest value in the BST without converting to an array. Output should be 3.

Hint: The minimum is the leftmost node. What comes next in inorder traversal?

Show Solution
int findSecondMin(TreeNode root) {
    // Do inorder traversal and stop at second node
    Stack stack = new Stack<>();
    TreeNode curr = root;
    int count = 0;
    
    while (curr != null || !stack.isEmpty()) {
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;
        }
        
        curr = stack.pop();
        count++;
        
        if (count == 2) {
            return curr.val;  // Second smallest!
        }
        
        curr = curr.right;
    }
    
    return -1;  // Less than 2 nodes
}

// For the example tree: inorder is [2, 3, 4, 5, 7, 8]
// Second smallest = 3

Given:

//       10
//      /  \
//     5    15
//    / \   / \
//   3   7 12  20
//
// low = 6, high = 15

Task: Return a list of all node values that fall within the range [6, 15] inclusive. Output should be [7, 10, 12, 15] (in sorted order).

Hint: Use BST property to prune unnecessary branches. No need to explore left if current < low!

Show Solution
List rangeBST(TreeNode root, int low, int high) {
    List result = new ArrayList<>();
    rangeHelper(root, low, high, result);
    return result;
}

void rangeHelper(TreeNode node, int low, int high, List result) {
    if (node == null) return;
    
    // Only go left if there could be valid nodes there
    if (node.val > low) {
        rangeHelper(node.left, low, high, result);
    }
    
    // Add current if in range
    if (node.val >= low && node.val <= high) {
        result.add(node.val);
    }
    
    // Only go right if there could be valid nodes there
    if (node.val < high) {
        rangeHelper(node.right, low, high, result);
    }
}

// Output: [7, 10, 12, 15]
// Note: Inorder traversal ensures sorted output!

Given:

// Tree 1:      Tree 2:
//     5            3
//    / \          / \
//   3   7        2   5
//  / \              / \
// 2   4            4   7

Task: Return true if both BSTs contain the exact same set of values, regardless of structure. Both trees above contain {2, 3, 4, 5, 7}, so return true.

Hint: What traversal gives you sorted order? Compare the sorted lists!

Show Solution
boolean containSameElements(TreeNode root1, TreeNode root2) {
    List list1 = new ArrayList<>();
    List list2 = new ArrayList<>();
    
    inorder(root1, list1);
    inorder(root2, list2);
    
    return list1.equals(list2);
}

void inorder(TreeNode node, List list) {
    if (node == null) return;
    inorder(node.left, list);
    list.add(node.val);
    inorder(node.right, list);
}

// Tree 1 inorder: [2, 3, 4, 5, 7]
// Tree 2 inorder: [2, 3, 4, 5, 7]
// They match! Return true

Given:

//       4
//      / \
//     2   5
//    / \
//   1   3

Task: Convert the BST to a circular doubly linked list in-place. The left pointer should point to the previous node, and the right pointer to the next node. Return the head (smallest node).

Hint: Do inorder traversal. Keep track of the previous node and link them together!

Show Solution
TreeNode first = null;  // Head of linked list (smallest)
TreeNode last = null;   // Tail of linked list (largest)

TreeNode treeToDoublyList(TreeNode root) {
    if (root == null) return null;
    
    first = null;
    last = null;
    
    inorder(root);
    
    // Make it circular
    last.right = first;
    first.left = last;
    
    return first;
}

void inorder(TreeNode node) {
    if (node == null) return;
    
    inorder(node.left);
    
    // Process current node
    if (last != null) {
        // Link previous node to current
        last.right = node;
        node.left = last;
    } else {
        // First node (smallest)
        first = node;
    }
    last = node;
    
    inorder(node.right);
}

// Result: 1 <-> 2 <-> 3 <-> 4 <-> 5 <-> (back to 1)
// first = node(1), last = node(5)

Given:

//       10
//      /  \
//     5    15
//    / \     \
//   3   7    20
//
// threshold = 10

Task: Find the sum of all nodes with values >= threshold. For threshold=10, sum nodes 10, 15, 20. Output should be 45.

Hint: Use BST property to skip left subtree when current node is already too small!

Show Solution
int sumGreaterOrEqual(TreeNode root, int threshold) {
    if (root == null) return 0;
    
    int sum = 0;
    
    // If current >= threshold, add it and check both subtrees
    if (root.val >= threshold) {
        sum = root.val;
        sum += sumGreaterOrEqual(root.left, threshold);
        sum += sumGreaterOrEqual(root.right, threshold);
    } else {
        // Current is too small, only right subtree can have valid nodes
        sum = sumGreaterOrEqual(root.right, threshold);
    }
    
    return sum;
}

// For threshold=10:
// - Node 10: >= 10, add 10, check both subtrees
// - Node 5: < 10, skip left subtree, check right only
// - Node 15: >= 10, add 15
// - Node 20: >= 10, add 20
// Total: 10 + 15 + 20 = 45

Given:

// Original BST:    Swapped BST (3 and 6 swapped):
//       5                   5
//      / \                 / \
//     3   7               6   7
//    /                   /
//   1                   1

Task: Two nodes in the BST were swapped by mistake. Find and swap them back to fix the BST without changing the tree structure.

Hint: Inorder of valid BST is sorted. Find the two anomalies where order breaks!

Show Solution
TreeNode first = null;   // First wrong node
TreeNode second = null;  // Second wrong node
TreeNode prev = null;    // Previous node in inorder

void recoverTree(TreeNode root) {
    first = second = prev = null;
    
    inorder(root);
    
    // Swap values of the two wrong nodes
    int temp = first.val;
    first.val = second.val;
    second.val = temp;
}

void inorder(TreeNode node) {
    if (node == null) return;
    
    inorder(node.left);
    
    // Check if order is broken
    if (prev != null && prev.val > node.val) {
        // Found a violation!
        if (first == null) {
            // First violation: prev is the first wrong node
            first = prev;
        }
        // Second is always the current node at violation
        second = node;
    }
    prev = node;
    
    inorder(node.right);
}

// Swapped BST inorder: [1, 6, 5, 7]
// Violation at 6 > 5: first=6, second=5
// After swap: [1, 3, 5, 7] - correct!

Interactive BST Demo

Experiment with BST operations! Insert values and watch the tree update in real-time.

Tree Info
Nodes: 0
Height: 0
Inorder: -

Insert values to build your BST!

Key Takeaways

Left < Root < Right

BST property: left subtree smaller, right subtree larger.

Inorder = Sorted

Inorder traversal of BST gives sorted sequence.

O(log n) Average

Search, insert, delete in O(log n) for balanced tree.

Delete with Successor

Replace with inorder successor when deleting node with 2 children.

Validate with Bounds

Pass min/max range to each node for O(n) validation.

Balance Matters

Skewed BST degrades to O(n) - use self-balancing trees for guarantees.

Knowledge Check

Question 1 of 6

What is the average time complexity of BST search?

Question 2 of 6

When deleting a BST node with two children, what replaces it?

Question 3 of 6

What traversal gives sorted output for BST?

Question 4 of 6

Where is the minimum element located in a BST?

Question 5 of 6

What is the worst-case time complexity of BST operations?

Question 6 of 6

What type of BST guarantees O(log n) operations?