← Back to DevBytes

Interview Guide: Binary Search Trees Problems and Solutions

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:

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:

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:

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:

# 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

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:

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles