Binary Search Tree — Interview Problems¶
Audience: Candidates preparing for senior software engineer interviews at FAANG-tier or comparable companies. Each problem is presented in the format used in real interviews: a precise statement, a discussion of approaches, and clean reference solutions in Go, Java, and Python. The trap and the easy-to-miss edge case are called out for every problem.
The BST family is one of the densest topics in coding interviews because the problems exercise three orthogonal skills at once: recursion on subtrees, the BST invariant as a pruning tool, and inorder traversal as a semantic. A single problem (LC 98, Validate BST) has been ranked among the top-20 most-asked Google interview questions for over a decade.
This document covers 11 representative problems.
Table of Contents¶
- LC 700 — Search in a BST
- LC 701 — Insert into a BST
- LC 450 — Delete Node in a BST
- LC 98 — Validate Binary Search Tree
- LC 230 — Kth Smallest Element in a BST
- LC 235 — Lowest Common Ancestor of a BST
- LC 173 — Binary Search Tree Iterator
- LC 108 — Convert Sorted Array to BST
- LC 1382 — Balance a BST
- LC 1008 — Construct BST from Preorder Traversal
- LC 99 — Recover Binary Search Tree
- LC 783 — Minimum Distance Between BST Nodes
1. LC 700 — Search in a BST (Easy)¶
Given the root of a BST and an integer
val, find the node in the BST whose value equalsval. Return the subtree rooted at that node; if no such node exists, returnnull.
Approach. Classic iterative or recursive BST descent. Both versions are O(h). Iterative is preferred for memory safety on tall trees.
Go:
func searchBST(root *TreeNode, val int) *TreeNode {
for root != nil && root.Val != val {
if val < root.Val {
root = root.Left
} else {
root = root.Right
}
}
return root
}
Java:
public TreeNode searchBST(TreeNode root, int val) {
while (root != null && root.val != val) {
root = (val < root.val) ? root.left : root.right;
}
return root;
}
Python:
def searchBST(self, root, val):
while root and root.val != val:
root = root.left if val < root.val else root.right
return root
Edge case — empty tree (root == null). The loop body never executes; returns null. Correct.
2. LC 701 — Insert into a BST (Medium)¶
Insert
valinto a BST and return the new root. The tree is guaranteed not to already containval.
Approach. Recursive "return new subtree root" idiom. Iterative version saves O(h) stack.
Go (recursive):
func insertIntoBST(root *TreeNode, val int) *TreeNode {
if root == nil { return &TreeNode{Val: val} }
if val < root.Val { root.Left = insertIntoBST(root.Left, val) }
if val > root.Val { root.Right = insertIntoBST(root.Right, val) }
return root
}
Java (iterative):
public TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) return new TreeNode(val);
TreeNode n = root;
while (true) {
if (val < n.val) {
if (n.left == null) { n.left = new TreeNode(val); return root; }
n = n.left;
} else {
if (n.right == null) { n.right = new TreeNode(val); return root; }
n = n.right;
}
}
}
Python (recursive):
def insertIntoBST(self, root, val):
if root is None:
return TreeNode(val)
if val < root.val:
root.left = self.insertIntoBST(root.left, val)
else:
root.right = self.insertIntoBST(root.right, val)
return root
Trap — forgetting to assign the recursive result. self.insertIntoBST(root.left, val) without root.left = ... loses the new node entirely.
3. LC 450 — Delete Node in a BST (Medium)¶
Remove the node with key
keyfrom a BST. Return the new root. Three cases: leaf, one child, two children. The two-children case is handled via Hibbard deletion (inorder successor).
Go:
func deleteNode(root *TreeNode, key int) *TreeNode {
if root == nil { return nil }
switch {
case key < root.Val:
root.Left = deleteNode(root.Left, key)
case key > root.Val:
root.Right = deleteNode(root.Right, key)
default:
if root.Left == nil { return root.Right }
if root.Right == nil { return root.Left }
succ := root.Right
for succ.Left != nil { succ = succ.Left }
root.Val = succ.Val
root.Right = deleteNode(root.Right, succ.Val)
}
return root
}
Java:
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
if (key < root.val) root.left = deleteNode(root.left, key);
else if (key > root.val) root.right = deleteNode(root.right, key);
else {
if (root.left == null) return root.right;
if (root.right == null) return root.left;
TreeNode succ = root.right;
while (succ.left != null) succ = succ.left;
root.val = succ.val;
root.right = deleteNode(root.right, succ.val);
}
return root;
}
Python:
def deleteNode(self, root, key):
if root is None:
return None
if key < root.val:
root.left = self.deleteNode(root.left, key)
elif key > root.val:
root.right = self.deleteNode(root.right, key)
else:
if root.left is None: return root.right
if root.right is None: return root.left
succ = root.right
while succ.left is not None: succ = succ.left
root.val = succ.val
root.right = self.deleteNode(root.right, succ.val)
return root
Trap — for the two-children case, candidates often try to swap the entire node (rewriting its .left and .right pointers) instead of just copying the value of the successor. The simpler "copy key, recursively delete successor" version is bug-proof and runs in O(h).
4. LC 98 — Validate Binary Search Tree (Medium) — THE CLASSIC TRAP¶
Given a binary tree, determine if it is a valid BST. Strict inequality on both sides.
This is the #1 BST interview problem and the one candidates fail most often. The trap is described in middle.md §7: comparing only direct children is wrong.
Solution A — Min/Max bounds¶
Java:
public boolean isValidBST(TreeNode root) {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean validate(TreeNode n, long lo, long hi) {
if (n == null) return true;
if (n.val <= lo || n.val >= hi) return false;
return validate(n.left, lo, n.val) &&
validate(n.right, n.val, hi);
}
Using long bounds is critical: a node with Integer.MAX_VALUE as its key must still validate, so the upper sentinel cannot equal Integer.MAX_VALUE.
Go:
import "math"
func isValidBST(root *TreeNode) bool {
var rec func(n *TreeNode, lo, hi float64) bool
rec = func(n *TreeNode, lo, hi float64) bool {
if n == nil { return true }
v := float64(n.Val)
if v <= lo || v >= hi { return false }
return rec(n.Left, lo, v) && rec(n.Right, v, hi)
}
return rec(root, math.Inf(-1), math.Inf(1))
}
Python:
def isValidBST(self, root):
def rec(n, lo=float('-inf'), hi=float('inf')):
if n is None: return True
if not (lo < n.val < hi): return False
return rec(n.left, lo, n.val) and rec(n.right, n.val, hi)
return rec(root)
Solution B — Inorder traversal¶
def isValidBST(self, root):
self.prev = float('-inf')
def inorder(n):
if n is None: return True
if not inorder(n.left): return False
if n.val <= self.prev: return False
self.prev = n.val
return inorder(n.right)
return inorder(root)
Both are O(n). Pick by taste; the min/max version generalizes to a broader pattern.
5. LC 230 — Kth Smallest Element in a BST (Medium)¶
Given a BST, return the k-th smallest value (1-indexed).
Approach. Inorder traversal with early stop. Iterative-with-stack is the idiomatic solution.
Python:
def kthSmallest(self, root, k):
stack = []
n = root
while n or stack:
while n:
stack.append(n); n = n.left
n = stack.pop()
k -= 1
if k == 0:
return n.val
n = n.right
Go:
func kthSmallest(root *TreeNode, k int) int {
stack := []*TreeNode{}
n := root
for n != nil || len(stack) > 0 {
for n != nil { stack = append(stack, n); n = n.Left }
n = stack[len(stack)-1]
stack = stack[:len(stack)-1]
k--
if k == 0 { return n.Val }
n = n.Right
}
return -1
}
Java:
public int kthSmallest(TreeNode root, int k) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode n = root;
while (n != null || !stack.isEmpty()) {
while (n != null) { stack.push(n); n = n.left; }
n = stack.pop();
if (--k == 0) return n.val;
n = n.right;
}
return -1;
}
Follow-up the interviewer will ask — what if the BST is modified often and you need many k-th queries? Answer: augment with subtree_size (see professional.md §6.1) for O(log n) k-th queries.
6. LC 235 — Lowest Common Ancestor of a BST (Medium)¶
Given a BST and two nodes
pandq, return their LCA. The LCA is the deepest node that has bothpandqas descendants (a node can be a descendant of itself).
Approach. Exploit the BST invariant: the LCA is the first node we encounter on the descent from root where p and q lie on different sides (one less, one greater) — or one of p / q is the node itself.
Go:
func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {
for root != nil {
switch {
case p.Val < root.Val && q.Val < root.Val:
root = root.Left
case p.Val > root.Val && q.Val > root.Val:
root = root.Right
default:
return root
}
}
return nil
}
Java:
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while (root != null) {
if (p.val < root.val && q.val < root.val) root = root.left;
else if (p.val > root.val && q.val > root.val) root = root.right;
else return root;
}
return null;
}
Python:
def lowestCommonAncestor(self, root, p, q):
while root:
if p.val < root.val and q.val < root.val:
root = root.left
elif p.val > root.val and q.val > root.val:
root = root.right
else:
return root
return None
Time O(h), space O(1) iteratively.
Contrast with LC 236 (general binary tree LCA): there you can't shortcut and must do a full DFS. The BST version is dramatically simpler.
7. LC 173 — Binary Search Tree Iterator (Medium)¶
Design a stateful iterator
BSTIteratorwithnext()returning the next smallest key andhasNext()returning whether more keys remain.next()must run in O(1) amortized time using at most O(h) memory.
Approach. Stack-based partial inorder traversal. Push the leftmost spine at construction; each next() pops one, then pushes the leftmost spine of its right child.
Python:
class BSTIterator:
def __init__(self, root):
self.stack = []
self._push_left(root)
def _push_left(self, node):
while node:
self.stack.append(node)
node = node.left
def next(self):
n = self.stack.pop()
self._push_left(n.right)
return n.val
def hasNext(self):
return bool(self.stack)
Go:
type BSTIterator struct{ stack []*TreeNode }
func Constructor(root *TreeNode) BSTIterator {
it := BSTIterator{}
it.pushLeft(root)
return it
}
func (it *BSTIterator) pushLeft(n *TreeNode) {
for n != nil { it.stack = append(it.stack, n); n = n.Left }
}
func (it *BSTIterator) Next() int {
n := it.stack[len(it.stack)-1]
it.stack = it.stack[:len(it.stack)-1]
it.pushLeft(n.Right)
return n.Val
}
func (it *BSTIterator) HasNext() bool { return len(it.stack) > 0 }
Java:
class BSTIterator {
private final Deque<TreeNode> stack = new ArrayDeque<>();
public BSTIterator(TreeNode root) { pushLeft(root); }
private void pushLeft(TreeNode n) {
while (n != null) { stack.push(n); n = n.left; }
}
public int next() {
TreeNode n = stack.pop();
pushLeft(n.right);
return n.val;
}
public boolean hasNext() { return !stack.isEmpty(); }
}
Amortized analysis. Each node is pushed and popped exactly once across all next() calls. n nodes → n pushes + n pops total → 2n operations spread over n calls = 2 ops per call amortized → O(1).
Space. Stack depth is bounded by O(h), not O(n). For balanced BSTs this is O(log n).
This pattern is the foundation of lazy iterators in C++'s std::map::iterator, Java's TreeMap.entrySet().iterator(), and similar STL/JCL iterators across languages.
8. LC 108 — Convert Sorted Array to BST (Easy)¶
Given a sorted array, build a height-balanced BST.
Approach. Recursive midpoint pick. The midpoint becomes the subtree root; recurse on left half and right half. Covered in detail in middle.md §9.
Python:
def sortedArrayToBST(self, nums):
def rec(lo, hi):
if lo > hi: return None
mid = (lo + hi) // 2
node = TreeNode(nums[mid])
node.left = rec(lo, mid - 1)
node.right = rec(mid + 1, hi)
return node
return rec(0, len(nums) - 1)
Go:
func sortedArrayToBST(nums []int) *TreeNode {
var rec func(lo, hi int) *TreeNode
rec = func(lo, hi int) *TreeNode {
if lo > hi { return nil }
mid := lo + (hi-lo)/2
return &TreeNode{Val: nums[mid], Left: rec(lo, mid-1), Right: rec(mid+1, hi)}
}
return rec(0, len(nums)-1)
}
Java:
public TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length - 1);
}
private TreeNode build(int[] nums, int lo, int hi) {
if (lo > hi) return null;
int mid = lo + (hi - lo) / 2;
TreeNode n = new TreeNode(nums[mid]);
n.left = build(nums, lo, mid - 1);
n.right = build(nums, mid + 1, hi);
return n;
}
Time O(n). Space O(log n) recursion + O(n) for the tree.
9. LC 1382 — Balance a BST (Medium)¶
Given a possibly-unbalanced BST, return a balanced BST with the same node values.
Approach. Two-pass: inorder traversal to extract sorted values, then LC 108 to rebuild balanced. Total O(n). Cannot do better without rotations.
Python:
def balanceBST(self, root):
vals = []
def inorder(n):
if n is None: return
inorder(n.left); vals.append(n.val); inorder(n.right)
inorder(root)
def build(lo, hi):
if lo > hi: return None
mid = (lo + hi) // 2
n = TreeNode(vals[mid])
n.left = build(lo, mid - 1)
n.right = build(mid + 1, hi)
return n
return build(0, len(vals) - 1)
Go:
func balanceBST(root *TreeNode) *TreeNode {
var vals []int
var inorder func(*TreeNode)
inorder = func(n *TreeNode) {
if n == nil { return }
inorder(n.Left); vals = append(vals, n.Val); inorder(n.Right)
}
inorder(root)
var build func(lo, hi int) *TreeNode
build = func(lo, hi int) *TreeNode {
if lo > hi { return nil }
mid := lo + (hi-lo)/2
return &TreeNode{Val: vals[mid], Left: build(lo, mid-1), Right: build(mid+1, hi)}
}
return build(0, len(vals)-1)
}
Java: analogous — left as exercise; same pattern.
Discussion. This is the "compaction" strategy real systems use occasionally when they want O(log n) lookups without per-operation balancing — accept that the tree drifts unbalanced, periodically rebuild. Simple to implement, predictable.
10. LC 1008 — Construct BST from Preorder Traversal (Medium)¶
Given the preorder traversal of a BST, reconstruct the tree.
Insight. In a preorder of a BST: first element is the root; the next contiguous prefix of values less than root form the preorder of the left subtree; the rest form the preorder of the right subtree.
O(n) approach using bounds:
def bstFromPreorder(self, preorder):
self.i = 0
def build(upper):
if self.i == len(preorder) or preorder[self.i] > upper:
return None
v = preorder[self.i]; self.i += 1
root = TreeNode(v)
root.left = build(v)
root.right = build(upper)
return root
return build(float('inf'))
Go:
func bstFromPreorder(preorder []int) *TreeNode {
i := 0
var build func(upper int) *TreeNode
build = func(upper int) *TreeNode {
if i == len(preorder) || preorder[i] > upper { return nil }
v := preorder[i]; i++
return &TreeNode{Val: v, Left: build(v), Right: build(upper)}
}
return build(1<<31 - 1)
}
Java:
class Solution {
int i = 0;
public TreeNode bstFromPreorder(int[] preorder) {
return build(preorder, Integer.MAX_VALUE);
}
private TreeNode build(int[] a, int upper) {
if (i == a.length || a[i] > upper) return null;
TreeNode root = new TreeNode(a[i++]);
root.left = build(a, root.val);
root.right = build(a, upper);
return root;
}
}
Time O(n) because each element is consumed once. The naive recursive "split on first-greater-than-root" version is O(n²) worst case.
11. LC 99 — Recover Binary Search Tree (Medium)¶
Exactly two nodes in a BST have been swapped. Recover the BST without changing the tree structure (only swap their values).
Approach. Inorder traversal of a valid BST yields a strictly increasing sequence. If two nodes are swapped, the sequence has either one or two "drops" (places where prev > curr): - Two adjacent nodes swapped → one drop, the swapped pair is (prev, curr). - Two non-adjacent nodes swapped → two drops. The first node is prev of the first drop; the second is curr of the second drop.
Python (Morris traversal for O(1) space is the bonus version; here's the simpler stack version):
def recoverTree(self, root):
first = second = prev = None
stack = []
n = root
while n or stack:
while n:
stack.append(n); n = n.left
n = stack.pop()
if prev and prev.val > n.val:
if first is None: first = prev
second = n
prev = n
n = n.right
first.val, second.val = second.val, first.val
Go:
func recoverTree(root *TreeNode) {
var first, second, prev *TreeNode
stack := []*TreeNode{}
n := root
for n != nil || len(stack) > 0 {
for n != nil { stack = append(stack, n); n = n.Left }
n = stack[len(stack)-1]; stack = stack[:len(stack)-1]
if prev != nil && prev.Val > n.Val {
if first == nil { first = prev }
second = n
}
prev = n
n = n.Right
}
first.Val, second.Val = second.Val, first.Val
}
Java:
class Solution {
TreeNode first, second, prev;
public void recoverTree(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode n = root;
while (n != null || !stack.isEmpty()) {
while (n != null) { stack.push(n); n = n.left; }
n = stack.pop();
if (prev != null && prev.val > n.val) {
if (first == null) first = prev;
second = n;
}
prev = n;
n = n.right;
}
int t = first.val; first.val = second.val; second.val = t;
}
}
Time O(n), space O(h) for the stack. Morris traversal can reduce space to O(1) — see CLRS §10.4-3 — at the cost of more complex code.
12. LC 783 — Minimum Distance Between BST Nodes (Easy)¶
Find the minimum absolute difference between values of any two nodes in a BST.
Insight. The minimum difference in a sorted sequence is always between adjacent elements. Inorder traversal yields sorted; track min(|curr - prev|) as we walk.
Python:
def minDiffInBST(self, root):
self.prev = None
self.ans = float('inf')
def inorder(n):
if n is None: return
inorder(n.left)
if self.prev is not None:
self.ans = min(self.ans, n.val - self.prev)
self.prev = n.val
inorder(n.right)
inorder(root)
return self.ans
Go:
func minDiffInBST(root *TreeNode) int {
prev := -1
ans := 1 << 30
var inorder func(*TreeNode)
inorder = func(n *TreeNode) {
if n == nil { return }
inorder(n.Left)
if prev >= 0 && n.Val - prev < ans {
ans = n.Val - prev
}
prev = n.Val
inorder(n.Right)
}
inorder(root)
return ans
}
Java:
class Solution {
Integer prev = null;
int ans = Integer.MAX_VALUE;
public int minDiffInBST(TreeNode root) {
inorder(root);
return ans;
}
private void inorder(TreeNode n) {
if (n == null) return;
inorder(n.left);
if (prev != null) ans = Math.min(ans, n.val - prev);
prev = n.val;
inorder(n.right);
}
}
Time O(n), space O(h).
Variant — LC 530 ("Minimum Absolute Difference in BST") is the exact same problem under a different number.
Interview Tips¶
- Start with the BST invariant. Reciting it precisely signals that you understand the structure, not just the operations.
- Use the min/max bounds pattern for validation. It's the right answer to LC 98 and to most invariant-checking problems.
- Use inorder traversal for any "sorted" problem. Kth-smallest, validation, two-sum-in-BST, recover BST, minimum distance — they all reduce to "walk the tree in order, look at adjacent pairs".
- For LCA, exploit the ordering. LC 235 has an O(h) iterative solution; LC 236 (binary tree LCA) needs DFS. Don't conflate.
- For the iterator pattern (LC 173), drive home that O(1) amortized comes from the n pushes + n pops argument. Interviewers love this analysis.
- For deletion, walk through Case A, B, C explicitly. Even if you write the code, the verbal explanation matters.
- Mention follow-ups proactively. "If we needed many kth-smallest queries, we'd augment with subtree sizes for O(log n) per query." Shows depth.
- Always sanity-check on tiny trees — empty, single node, two nodes, balanced 3-node, skewed 3-node. Most bugs surface immediately.
Good luck.