What Is a Binary Search Tree?
A Binary Search Tree (BST) is a node-based binary tree data structure where each node has at most two children, referred to as the left child and the right child. The defining property of a BST is the search invariant:
- For any given node, all values in its left subtree are strictly less than the node's value.
- All values in its right subtree are strictly greater than the node's value.
- This property must hold recursively for every node in the tree.
This ordering allows for efficient searching, insertion, and deletion operations—all running in O(h) time, where h is the height of the tree. In a balanced BST, h ≈ log n, giving O(log n) performance. In the worst case (a degenerate tree that resembles a linked list), h = n, yielding O(n) operations.
# Basic BST Node definition in Python
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
Visual Representation of BST Ordering
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
# The BST property: 1 < 3 < 4 < 6 < 7 < 8 < 10 < 13 < 14
Why BSTs Matter in Technical Interviews
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →BST problems appear with remarkable frequency in coding interviews at companies of all sizes. Interviewers favor BST questions for several compelling reasons:
- Recursive thinking: BST problems naturally lend themselves to recursive solutions, testing a candidate's comfort with recursion and the call stack.
- Tree traversal mastery: They require fluent use of inorder, preorder, and postorder traversals, often with modifications to track state.
- BST property exploitation: The best solutions leverage the BST invariant to prune search paths, turning O(n) brute-force approaches into O(log n) or O(h) solutions.
- Edge case richness: Empty trees, single-node trees, skewed trees, duplicate values (or the lack thereof), and boundary conditions expose whether a candidate thinks comprehensively.
- Real-world relevance: BSTs underpin database indexes, file system structures, and in-memory sorted containers.
BST Properties and Essential Traversals
Before diving into specific problems, you must internalize three fundamental traversal patterns and how the BST property interacts with each:
Inorder Traversal (Left → Node → Right)
An inorder traversal of a BST visits nodes in ascending sorted order. This is the single most important fact for BST interview problems—it transforms the tree into a sorted sequence.
def inorder(node, result=None):
if result is None:
result = []
if node is None:
return result
inorder(node.left, result)
result.append(node.val)
inorder(node.right, result)
return result
# For the tree above: inorder yields [1, 3, 4, 6, 7, 8, 10, 13, 14]
Preorder Traversal (Node → Left → Right)
Preorder is useful for serializing/deserializing BSTs because the root comes first, preserving the structural information needed to reconstruct the tree.
def preorder(node, result=None):
if result is None:
result = []
if node is None:
return result
result.append(node.val)
preorder(node.left, result)
preorder(node.right, result)
return result
Postorder Traversal (Left → Right → Node)
Postorder is essential when you need to process children before the parent—common in deletion scenarios and when computing subtree aggregates (sums, heights, validations).
def postorder(node, result=None):
if result is None:
result = []
if node is None:
return result
postorder(node.left, result)
postorder(node.right, result)
result.append(node.val)
return result
Common BST Interview Problems with Complete Solutions
Problem 1: Validate a Binary Search Tree
Goal: Determine whether a given binary tree satisfies the BST property. This is arguably the most asked BST question. The naive approach—comparing each node only with its immediate children—fails because a value deep in the right subtree might violate the BST property relative to a distant ancestor. The correct solution passes down a valid range (min, max) that each node must fall within.
Approach 1: Recursive Range Validation
def isValidBST(root: TreeNode) -> bool:
def validate(node, low, high):
if node is None:
return True
# Current node's value must be strictly between low and high
if low is not None and node.val <= low:
return False
if high is not None and node.val >= high:
return False
# Recurse: left subtree gets (low, node.val), right gets (node.val, high)
return (validate(node.left, low, node.val) and
validate(node.right, node.val, high))
return validate(root, None, None)
# Time: O(n) — visits each node once
# Space: O(h) — recursion stack height
Approach 2: Inorder Traversal Check
Since an inorder traversal of a valid BST produces a strictly increasing sequence, we can track the previous value and ensure each subsequent value is larger.
def isValidBST_inorder(root: TreeNode) -> bool:
prev = [float('-inf')] # Use list to allow modification in nested scope
def inorder_check(node):
if node is None:
return True
if not inorder_check(node.left):
return False
if node.val <= prev[0]:
return False
prev[0] = node.val
return inorder_check(node.right)
return inorder_check(root)
# Alternative: explicit stack iterative version
def isValidBST_iterative(root: TreeNode) -> bool:
stack = []
prev = float('-inf')
current = root
while stack or current:
# Go as far left as possible
while current:
stack.append(current)
current = current.left
current = stack.pop()
# Check BST property
if current.val <= prev:
return False
prev = current.val
current = current.right
return True
Problem 2: Search and Insert in a BST
Search: Exploit the BST property to prune half the tree at each step.
def search_bst(root: TreeNode, target: int) -> TreeNode | None:
current = root
while current:
if target == current.val:
return current
elif target < current.val:
current = current.left
else:
current = current.right
return None
# Recursive version
def search_bst_recursive(root: TreeNode, target: int) -> TreeNode | None:
if root is None:
return None
if target == root.val:
return root
if target < root.val:
return search_bst_recursive(root.left, target)
return search_bst_recursive(root.right, target)
Insert: Walk down the tree following the BST rule until you hit a null child, then insert there.
def insert_into_bst(root: TreeNode, val: int) -> TreeNode:
if root is None:
return TreeNode(val)
current = root
while True:
if val < current.val:
if current.left is None:
current.left = TreeNode(val)
break
current = current.left
else: # val > current.val (assuming no duplicates)
if current.right is None:
current.right = TreeNode(val)
break
current = current.right
return root
# Recursive insert (returns new subtree root)
def insert_bst_recursive(root: TreeNode, val: int) -> TreeNode:
if root is None:
return TreeNode(val)
if val < root.val:
root.left = insert_bst_recursive(root.left, val)
else:
root.right = insert_bst_recursive(root.right, val)
return root
Problem 3: Delete a Node from a BST
Goal: Remove a node with a given key while maintaining the BST property. This is the most complex basic BST operation, with three cases:
- Case 1: Node is a leaf — simply remove it.
- Case 2: Node has one child — replace node with its child.
- Case 3: Node has two children — find the inorder successor (smallest node in the right subtree), swap values, then delete that successor (which falls into Case 1 or 2).
def delete_node(root: TreeNode, key: int) -> TreeNode:
if root is None:
return None
# Search phase: find the node to delete
if key < root.val:
root.left = delete_node(root.left, key)
elif key > root.val:
root.right = delete_node(root.right, key)
else:
# Node found — handle the three cases
# Case 1 & 2: node has 0 or 1 child
if root.left is None:
return root.right
if root.right is None:
return root.left
# Case 3: node has two children
# Find inorder successor (minimum in right subtree)
successor = find_min(root.right)
# Copy successor's value to current node
root.val = successor.val
# Delete the successor from the right subtree
root.right = delete_node(root.right, successor.val)
return root
def find_min(node: TreeNode) -> TreeNode:
while node and node.left:
node = node.left
return node
# Iterative approach for clarity
def delete_node_iterative(root: TreeNode, key: int) -> TreeNode:
if root is None:
return None
# Find the node and its parent
parent = None
current = root
while current and current.val != key:
parent = current
if key < current.val:
current = current.left
else:
current = current.right
if current is None: # Key not found
return root
# Case 3 helper: get successor and its parent
def get_successor(node, node_parent):
succ_parent = node_parent
succ = node.right
while succ and succ.left:
succ_parent = succ
succ = succ.left
return succ, succ_parent
# If node has two children
if current.left and current.right:
succ, succ_parent = get_successor(current, parent)
current.val = succ.val
# Now delete succ (it has at most one child)
current = succ
parent = succ_parent
# Now current has at most one child
child = current.left if current.left else current.right
if parent is None:
return child # Deleting root
if parent.left == current:
parent.left = child
else:
parent.right = child
return root
Problem 4: Inorder Successor in a BST
Goal: Given a node, find the next node in an inorder traversal. This tests deep understanding of BST structure. There are two sub-cases:
- If the node has a right child, the successor is the leftmost node in the right subtree.
- If the node has no right child, traverse up via parent pointers until we find an ancestor whose left child is on our path; that ancestor is the successor.
# With parent pointers
def inorder_successor_with_parent(node: TreeNode) -> TreeNode | None:
if node is None:
return None
# Case 1: right child exists
if node.right:
return find_min(node.right)
# Case 2: go up until we're coming from a left child
current = node
parent = node.parent # hypothetical parent reference
while parent and parent.right == current:
current = parent
parent = parent.parent
return parent
# Without parent pointers — start from root
def inorder_successor(root: TreeNode, target: TreeNode) -> TreeNode | None:
if target.right:
return find_min(target.right)
successor = None
current = root
while current:
if target.val < current.val:
# Current is a potential successor, go left
successor = current
current = current.left
elif target.val > current.val:
current = current.right
else:
break # Found the target node
return successor
Problem 5: Lowest Common Ancestor (LCA) in a BST
Goal: Find the lowest node that has both p and q as descendants. The BST property makes this much simpler than the general binary tree LCA: we can use the values to decide which way to traverse.
def lowestCommonAncestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
current = root
while current:
# If both values are less than current, LCA must be in left subtree
if p.val < current.val and q.val < current.val:
current = current.left
# If both values are greater than current, LCA must be in right subtree
elif p.val > current.val and q.val > current.val:
current = current.right
else:
# Split or equal: current is the LCA
return current
return None
# Recursive version
def lca_recursive(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
if root is None:
return None
if p.val < root.val and q.val < root.val:
return lca_recursive(root.left, p, q)
if p.val > root.val and q.val > root.val:
return lca_recursive(root.right, p, q)
return root
Problem 6: Construct BST from Sorted Array
Goal: Given a sorted (increasing) array of unique integers, build a height-balanced BST. The key insight: pick the middle element as the root, recursively build left and right subtrees from the left and right halves of the array.
def sortedArrayToBST(nums: list[int]) -> TreeNode:
def build(left: int, right: int) -> TreeNode | None:
if left > right:
return None
mid = left + (right - left) // 2 # Avoid overflow
root = TreeNode(nums[mid])
root.left = build(left, mid - 1)
root.right = build(mid + 1, right)
return root
return build(0, len(nums) - 1)
# Example: nums = [1, 3, 4, 6, 7, 8, 10, 13, 14]
# Produces a balanced BST with height ~ log2(9) ≈ 4
Problem 7: Kth Smallest Element in a BST
Goal: Find the kth smallest value (1-indexed). Leverage inorder traversal: the kth node visited in inorder is the answer.
def kthSmallest(root: TreeNode, k: int) -> int:
stack = []
current = root
count = 0
while stack or current:
while current:
stack.append(current)
current = current.left
current = stack.pop()
count += 1
if count == k:
return current.val
current = current.right
return -1 # k is out of range
# Recursive with early termination
def kthSmallest_recursive(root: TreeNode, k: int) -> int:
result = [None]
count = [0]
def inorder(node):
if node is None or result[0] is not None:
return
inorder(node.left)
count[0] += 1
if count[0] == k:
result[0] = node.val
return
inorder(node.right)
inorder(root)
return result[0] if result[0] is not None else -1
# Follow-up: if the tree is frequently modified, augment nodes with
# subtree size to achieve O(h) queries
class AugmentedTreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
self.size = 1 # Number of nodes in subtree rooted at this node
def kthSmallest_augmented(root: AugmentedTreeNode, k: int) -> int:
left_size = root.left.size if root.left else 0
if k == left_size + 1:
return root.val
elif k <= left_size:
return kthSmallest_augmented(root.left, k)
else:
return kthSmallest_augmented(root.right, k - left_size - 1)
Problem 8: Range Sum of BST
Goal: Given inclusive bounds [low, high], return the sum of all node values within that range. Use the BST property to prune irrelevant subtrees.
def rangeSumBST(root: TreeNode, low: int, high: int) -> int:
if root is None:
return 0
total = 0
# If current value is within range, include it
if low <= root.val <= high:
total += root.val
# If current value is greater than low, left subtree might have values in range
if root.val > low:
total += rangeSumBST(root.left, low, high)
# If current value is less than high, right subtree might have values in range
if root.val < high:
total += rangeSumBST(root.right, low, high)
return total
# Iterative stack version
def rangeSumBST_iterative(root: TreeNode, low: int, high: int) -> int:
if root is None:
return 0
stack = [root]
total = 0
while stack:
node = stack.pop()
if low <= node.val <= high:
total += node.val
if node.val > low and node.left:
stack.append(node.left)
if node.val < high and node.right:
stack.append(node.right)
return total
Problem 9: Convert BST to Greater Sum Tree
Goal: Transform a BST so that every node's value becomes the sum of all values greater than or equal to its original value. This requires a reverse inorder traversal (right → node → left), accumulating a running sum.
def convertBST(root: TreeNode) -> TreeNode:
running_sum = [0]
def reverse_inorder(node):
if node is None:
return
reverse_inorder(node.right)
running_sum[0] += node.val
node.val = running_sum[0]
reverse_inorder(node.left)
reverse_inorder(root)
return root
Problem 10: Find Mode(s) in a BST
Goal: Find all values that appear most frequently. An inorder traversal lets us track streaks since equal values (if allowed) appear consecutively in sorted order.
def findMode(root: TreeNode) -> list[int]:
modes = []
max_streak = 0
current_val = None
current_streak = 0
def handle_value(val):
nonlocal modes, max_streak, current_val, current_streak
if val == current_val:
current_streak += 1
else:
current_val = val
current_streak = 1
if current_streak > max_streak:
max_streak = current_streak
modes = [val]
elif current_streak == max_streak:
modes.append(val)
def inorder(node):
if node is None:
return
inorder(node.left)
handle_value(node.val)
inorder(node.right)
inorder(root)
return modes
Best Practices for BST Interview Problems
1. Always Clarify the BST Definition
Before writing any code, ask your interviewer: "Are duplicate values allowed? If so, where do they go—left or right?" Some implementations allow duplicates on the left (<=), others on the right (>=), and some forbid them entirely. This decision affects validation, insertion, and deletion logic.
2. Exploit the BST Property Explicitly
The most common mistake candidates make is treating a BST problem like a generic binary tree problem and missing the O(h) optimization. Always ask yourself: "Can I eliminate an entire subtree based on a value comparison?" If you find yourself traversing both subtrees unconditionally, reconsider—you may be missing a pruning opportunity.
3. Master Both Recursive and Iterative Approaches
Recursive solutions are elegant and align naturally with tree structures, but iterative solutions (using explicit stacks or while loops) demonstrate deeper control and avoid stack overflow on skewed trees. Practice both forms for every problem—interviewers often ask for the other style as a follow-up.
4. Handle Edge Cases Systematically
- Empty tree (root is null): Always the first check—return appropriate sentinel value.
- Single node tree: Validate that your logic doesn't assume multiple nodes.
- Skewed/linear tree: Test on a tree that is essentially a linked list (all left children or all right children).
- Value not found: For search/delete problems, define behavior when the target doesn't exist.
- Boundary values: For range queries, test with values exactly at low/high boundaries.
5. Use the Inorder Property as a Secret Weapon
Many BST problems that seem complex become trivial when you recognize that an inorder traversal gives you a sorted array. Problems like "kth smallest," "find mode," "validate BST," and "convert to greater sum tree" all yield to inorder-based solutions. Keep this in your mental toolbox and mention it during problem analysis.
6. Communicate Complexity Clearly
Always state time and space complexity. For BST problems:
- Time: Typically O(h) for search/insert operations on a balanced tree (O(log n)), or O(n) for full traversals.
- Space: O(h) for recursion stack (or O(1) for true iterative Morris traversal).
- If the tree is unbalanced, mention the worst-case O(n) time/space degradation.
7. Consider Augmented Node Structures
For follow-up questions about optimizing frequent queries (like "kth smallest with dynamic updates"), suggest augmenting nodes with metadata like subtree size, min/max ranges, or parent pointers. This shows systems-level thinking beyond the basic algorithm.
8. Write Clean, Modular Helper Functions
Extract reusable operations like find_min, find_max, or subtree_count into their own functions. This makes your code more readable, easier to debug, and signals to the interviewer that you organize code professionally.
# Good: clean helper functions
def find_min(node: TreeNode) -> TreeNode:
while node and node.left:
node = node.left
return node
def find_max(node: TreeNode) -> TreeNode:
while node and node.right:
node = node.right
return node
# Now use them in your main logic
def delete_node(root: TreeNode, key: int) -> TreeNode:
# ... uses find_min for successor case
pass
9. Practice the Morris Traversal
The Morris traversal achieves O(1) space complexity for inorder/preorder traversals by temporarily modifying tree pointers. While not required for most interviews, knowing it demonstrates advanced preparation and can be a powerful differentiator.
def morris_inorder(root: TreeNode) -> list[int]:
result = []
current = root
while current:
if current.left is None:
result.append(current.val)
current = current.right
else:
# Find the inorder predecessor (rightmost in left subtree)
predecessor = current.left
while predecessor.right and predecessor.right != current:
predecessor = predecessor.right
if predecessor.right is None:
# Create a temporary thread back to current
predecessor.right = current
current = current.left
else:
# Thread already exists, remove it and visit current
predecessor.right = None
result.append(current.val)
current = current.right
return result
10. Test Your Code Verbally
Before declaring a solution complete, walk through it with a small concrete tree. Say the values aloud: "At the root 8, both p=3 and q=10 are split across, so 8 is the LCA." This catches logic errors and demonstrates thoroughness. Interviewers consistently rank candidates who test their code higher than those who don't.
Conclusion
Binary Search Trees represent a beautiful intersection of data structure design and algorithmic thinking. Their ordered nature provides the leverage to turn O(n) problems into O(log n) solutions—but only if you internalize the BST invariant and reach for it instinctively. The problems covered here—validation, search, insertion, deletion, successor finding, LCA, construction from sorted data, kth smallest, range queries, and transformation—form the core repertoire that appears across technical interviews. Master both recursive and iterative implementations for each, build fluency with the inorder traversal as a sorted-sequence generator, and always clarify the exact BST contract (duplicate policy, balanced vs. unbalanced) before coding. With deliberate practice on these patterns, you'll approach any BST interview problem with confidence and precision.