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!
For ANY node in the tree:
This seems simple, but there's a catch...
The rule applies to the ENTIRE subtree, not just direct children.
ALL of these must be < 8!
Beginners often check only parent-child relationships!
INVALID: 7 > 5 but in left subtree!
Traverse in Left β Root β Right order and you get a sorted sequence!
Standard BSTs don't allow duplicate values.
| 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);
}
Search Operation
Searching in a BST is like playing a guessing game where someone tells you "higher" or "lower" after each guess. You start at the root and make a simple decision at each step: if your target is smaller, go left; if larger, go right. This eliminates half the remaining tree with each comparison!
Why is BST Search So Fast?
In a balanced BST with 1 million nodes, you need at most 20 comparisons to find any value (because log2(1,000,000) β 20). Compare this to a linked list where you might need to check all 1 million nodes!
Key insight: Each comparison eliminates roughly half the remaining nodes. This is the same principle behind binary search in sorted arrays - the BST just organizes data in a tree structure.
TreeNode search(TreeNode root, int target) {
// Base case: empty tree or found the target
if (root == null || root.val == target) {
return root;
}
// Decide direction based on BST property
if (target < root.val) {
return search(root.left, target); // Go left
} else {
return search(root.right, target); // Go right
}
}
root == null β Not foundroot.val == target β Found!
When target < root.val
Target must be in left subtree
When target > root.val
Target must be in right subtree
The recursive approach is the most intuitive way to search a BST. Think of it like giving directions: "If you're looking for a smaller number, go left. If you're looking for a larger number, go right. Keep following these directions until you find what you're looking for or reach a dead end." The function calls itself with a smaller subtree each time, naturally eliminating half of the remaining nodes with every recursive call. This elegant solution directly mirrors how we think about the problem!
TreeNode searchIterative(TreeNode root, int target) {
// Keep going until we find target or hit null
while (root != null && root.val != target) {
if (target < root.val) {
root = root.left; // Move left
} else {
root = root.right; // Move right
}
}
return root; // Either found node or null
}
No recursion stack β O(1) space instead of O(h)
Better for very deep trees to avoid stack overflow
Continue while: root != null AND root.val != target
Exit when found OR reached dead end
The iterative approach does exactly the same thing as recursion, but uses a while loop instead of function calls. Why would you prefer this? Space efficiency! Recursive calls build up on the call stack - in a very deep tree (like one with 10,000 levels), you could get a stack overflow. The iterative version uses constant O(1) space regardless of tree depth. Interviewers often ask candidates to convert recursive solutions to iterative - this is a perfect example of that skill!
TreeNode findMin(TreeNode root) {
while (root.left != null) {
root = root.left;
}
return root;
}
Keep going left until no more left children
TreeNode findMax(TreeNode root) {
while (root.right != null) {
root = root.right;
}
return root;
}
Keep going right until no more right children
Finding the minimum and maximum values in a BST is beautifully simple thanks to the BST property! Remember: in a BST, all smaller values are to the left and all larger values are to the right. So where's the smallest value? At the leftmost node - just keep going left until you can't anymore! Where's the largest? At the rightmost node. No comparisons needed, no checking values - just follow the path. These helper functions are used heavily in BST deletion (to find the inorder successor) and in many interview problems!
How Search Works - Detailed Walkthrough
Think of each node as a signpost that tells you which direction to go. You never need to explore both sides - the BST property guarantees the target can only be in ONE direction!
This is your entry point into the tree. Compare your target value with the root's value to decide where to go next.
Go LEFT! All smaller values are guaranteed in the left subtree. Completely ignore the right side!
Go RIGHT! All larger values are guaranteed in the right subtree. Completely ignore the left side!
Values match! Return this node. Search is complete with success.
You've reached a dead end - the value doesn't exist in this tree. Return null.
Recursive is cleaner, but iterative uses O(1) space instead of O(h) stack space.
Min = leftmost node (keep going left). Max = rightmost node (keep going right). No comparisons needed!
Visual: Searching for Value 6
6 < 8
β
Go LEFT
6 > 3
β
Go RIGHT
6 == 6
β
FOUND!
Practice Questions
Problem: Find the value in BST closest to the target. Assume BST is not empty.
// Input: root = [8,3,10,1,6,null,14], target = 5
// Output: 6 (could also be 3, both have distance 2)
View Solution
int closestValue(TreeNode root, double target) {
int closest = root.val;
while (root != null) {
if (Math.abs(root.val - target) < Math.abs(closest - target)) {
closest = root.val;
}
root = target < root.val ? root.left : root.right;
}
return closest;
}
Problem: Find the floor (largest value <= target) and ceiling (smallest value >= target).
// Input: root = [8,3,10,1,6,null,14], target = 5
// Output: floor = 3, ceiling = 6
View Solution
Integer floor(TreeNode root, int target) {
Integer floor = null;
while (root != null) {
if (root.val == target) return target;
if (root.val < target) {
floor = root.val;
root = root.right;
} else {
root = root.left;
}
}
return floor;
}
Integer ceiling(TreeNode root, int target) {
Integer ceiling = null;
while (root != null) {
if (root.val == target) return target;
if (root.val > target) {
ceiling = root.val;
root = root.left;
} else {
root = root.right;
}
}
return ceiling;
}
Problem: Search for a value and return the number of comparisons made.
// Input: root = [8,3,10,1,6,null,14], target = 6
// Output: 3 comparisons (8 -> 3 -> 6)
View Solution
int searchWithCount(TreeNode root, int target) {
int comparisons = 0;
while (root != null) {
comparisons++;
if (root.val == target) {
return comparisons; // Found!
} else if (target < root.val) {
root = root.left;
} else {
root = root.right;
}
}
return -comparisons; // Negative indicates not found
}
Problem: Given a node in BST, find its inorder successor (next largest value).
// Input: root = [8,3,10,1,6,null,14], node = 6
// Output: 8 (next value after 6 in inorder)
View Solution
TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
TreeNode successor = null;
while (root != null) {
if (p.val < root.val) {
// Current could be successor, go left to find smaller
successor = root;
root = root.left;
} else {
// p.val >= root.val, go right
root = root.right;
}
}
return successor;
}
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.
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
}
root == null β Create new node
This is where insertion happens!
When val < root.val
Assign result to root.left
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!
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;
}
Keep parent pointer
We need it to attach the new node
Loop until curr == null
This is where new node belongs
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?"
TreeNode buildBST(int[] nums) {
TreeNode root = null;
for (int num : nums) {
root = insert(root, num); // Insert each element
}
return root;
}
Initialize root = null
First insert creates the root node
Insert each array element one by one
Tree grows with each insertion
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!
The new node becomes the root. This is your base case - when root == null, return a new node!
If new value < current β go left. If new value > current β go right.
Call recursively: root.left = insert(root.left, val). The returned value updates the child pointer.
When we reach an empty spot (null), create and return the new node. The parent's assignment connects it to the tree automatically!
Each recursive call returns the (unchanged) root of its subtree. This "bubbles up" to return the original root with the new node attached.
The pattern root.left = insert(root.left, val); return root; is elegant:
Visual: Inserting Value 5
5 < 8β left
5 > 3β right
5 < 6β left
INSERT
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);
}
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.
1. Leaf node: Simply remove it
2. One child: Replace with child
3. Two children: Replace with inorder successor (or predecessor)
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;
}
No children β return null
Parent's pointer becomes null
Return the single child
Grandchild replaces deleted node
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!
TreeNode findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
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)!
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;
}
Successor: smallest in right subtree
Predecessor: largest in left subtree
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!
The simplest case! The node has no children, so removing it won't break any connections.
Return null to parent. Parent's root.left = delete(...) sets child to null, removing the node.
Node has exactly one child. We can "bypass" the node by connecting parent directly to grandchild.
Return the single child to parent. Grandchild becomes new child. BST property preserved since entire subtree was already valid!
Can't just remove - both subtrees would disconnect! Need a replacement value.
- Find inorder successor
- Copy its value to node
- Delete the successor
The inorder successor is the SMALLEST value that is LARGER than the deleted node. When we move it up:
Everything was smaller than original, successor is larger than original β all still smaller than successor
Successor was the smallest in right subtree β all others in right are larger than successor
Visual: The Three Delete Cases
No children - just remove it!
Replace node with its only child
Find inorder successor, swap & delete
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;
}
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
Why 3 is WRONG here:
- Local check passes:
3 < 6(parent check OK) - Global check fails:
3 < 5but 3 is in right subtree of 5! - Rule: All nodes in right subtree must be > 5
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);
}
Each node must be:min < node.val < max
Update max = node.val
All left descendants < parent
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.
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;
}
Inorder traversal of a valid BST produces sorted order. If any value β€ previous, it's invalid!
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!
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);
}
prev persists across recursive calls to track the last visited node in inorder.
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:
| Method | How It Works | Pros/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. |
Practice Questions
Problem: Identify why this tree is NOT a valid BST:
// 10
// / \
// 5 15
// / \ / \
// 3 12 12 20
View Solution
Two issues:
- Node 12 in left subtree of 10 violates BST property (12 > 10 but in left subtree)
- 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
}
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!
In a BST, inorder traversal (Left β Root β Right) visits nodes in sorted order!
- Kth smallest/largest element
- Range sum queries
- Check if BST is valid
- Convert BST to sorted list
Use BST property to navigate: smaller β left, larger β right. No need to check both sides!
- Lowest Common Ancestor
- Inorder successor/predecessor
- Search, Insert, Delete
- Floor/Ceiling of a value
Break into: left subtree + root + right subtree. Combine results recursively!
- Build BST from sorted array
- Validate BST structure
- Serialize/Deserialize BST
- Balance a BST
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).
Why Inorder Traversal?
In a BST, inorder traversal (Left β Root β Right) visits nodes in sorted order!
Algorithm: Do inorder traversal, count nodes. When count equals k, that's your answer!
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
}
Inorder = sorted order!
Just count to k during traversal
Push all left nodes to stack
Reaches smallest element first
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.
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;
}
Inorder: Left β Root β Right (ascending)
Reverse: Right β Root β Left (descending)
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)
Why BST Makes LCA Easy:
In a BST, we can find LCA in O(log n) without checking both subtrees!
- 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
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 p,q are smaller
LCA must be in left subtree
Both p,q are larger
LCA must be in right subtree
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!
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
}
}
Same logic as iterative, just cleaner code. Base case: when nodes split, return current root.
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!
Sorted order: 1, 3, 4, 5, 7
Use cases: BST deletion (two-children case), implementing iterators, finding next/prev in sorted data.
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;
}
Root is larger than p β potential successor!
Save it and go left to find smaller one
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!
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;
}
Root is smaller than p β potential predecessor!
Save it and go right to find larger one
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!
The Key Insight:
Always pick the MIDDLE element as root!
- Middle of [1,2,3,4,5,6,7] = 4 β root
- Left half [1,2,3] β build left subtree
- Right half [5,6,7] β build right subtree
- Recurse until arrays are empty
Why balanced? Middle splits array in half β equal nodes on each side!
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;
}
mid = (left + right) / 2
Middle element becomes root
[left, mid-1]
Build left subtree recursively
[mid+1, right]
Build right subtree recursively
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]
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!