Menu
DSA interview questionsQuestion 86 of 147

DSA interview question · Question 86 of 147

Lowest Common Ancestor of a BST: Follow the Split Point

  • Medium
  • coding
  • ~10 min
  • High relevance
  • 5 min read
  • Updated Oct 2026

Short answer

Start at the root. If both p and q are smaller than the current value, the ancestor is in the left subtree; if both are larger, it is in the right subtree. Otherwise they split here (or one of them is the current node), so the current node is the lowest common ancestor. This follows a single root-to-node path: O(h) time and O(1) space iteratively, which is O(log n) for a balanced tree.

On this page
  1. Problem
  2. Examples
  3. Approach 1: brute force
  4. Approach 2: optimal
  5. Iterative
  6. Recursive
  7. Tests
  8. Edge cases and pitfalls
  9. Where this shows up in data engineering

Problem

You are given the root of a binary search tree with distinct values and two different nodes p and q that are in the tree. Return their lowest common ancestor: the deepest node that has both as descendants, where a node is a descendant of itself.

This is widely known as LeetCode 235 (Lowest Common Ancestor of a Binary Search Tree). Compared with the general binary tree version, the ordering lets you avoid searching both subtrees.

Constraints for this version: 2 to 100,000 nodes, distinct values, p and q both present.

Examples

BST built by inserting 30, 15, 45, 8, 22, 40, 60, 19, 25:

            30
          /    \
        15      45
       /  \    /  \
      8   22  40   60
         /  \
        19   25
p q LCA Why
19 25 22 they split at 22
8 25 15 8 is left of 15, 25 is right of 15
22 25 22 22 is an ancestor of 25
19 60 30 they split at the root

Approach 1: brute force

Ignore the ordering and use the general binary tree method: recurse into both subtrees and return the node where both sides report a match.

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def lca_general(root, p, q):
    if root is None or root is p or root is q:
        return root
    left = lca_general(root.left, p, q)
    right = lca_general(root.right, p, q)
    if left and right:
        return root
    return left or right

O(n) time because it may visit every node, and O(h) stack.

Approach 2: optimal

Key insight. In a BST, all values in the left subtree of a node are smaller than it and all in the right subtree are larger. Walking from the root, as long as both targets are on the same side, their common ancestors continue on that side. The first node where they are not on the same side (one is smaller and one larger, or one equals the node) is the lowest common ancestor.

Walkthrough for p = 19, q = 25:

Node Both smaller? Both larger? Action
30 yes no go left
15 no yes go right
22 no (19 is smaller, 25 larger) no split: answer 22

Iterative

def lowest_common_ancestor(root, p, q):
    node = root
    while node:
        if p.val < node.val and q.val < node.val:
            node = node.left
        elif p.val > node.val and q.val > node.val:
            node = node.right
        else:
            return node
    return None

Recursive

def lowest_common_ancestor_recursive(root, p, q):
    if p.val < root.val and q.val < root.val:
        return lowest_common_ancestor_recursive(root.left, p, q)
    if p.val > root.val and q.val > root.val:
        return lowest_common_ancestor_recursive(root.right, p, q)
    return root

Complexity. O(h) time: O(log n) for a balanced tree, O(n) for a skewed one. The iterative version uses O(1) space; the recursive one uses O(h) stack.

Tests

import random

def bst_from_inserts(values):
    root, nodes = None, {}
    for v in values:
        new = TreeNode(v)
        nodes[v] = new
        if root is None:
            root = new
            continue
        node = root
        while True:
            side = "left" if v < node.val else "right"
            child = getattr(node, side)
            if child is None:
                setattr(node, side, new)
                break
            node = child
    return root, nodes

root, nodes = bst_from_inserts([30, 15, 45, 8, 22, 40, 60, 19, 25])
cases = [(19, 25, 22), (8, 25, 15), (22, 25, 22), (19, 60, 30), (40, 60, 45), (30, 8, 30)]
for fn in (lca_general, lowest_common_ancestor, lowest_common_ancestor_recursive):
    for a, b, want in cases:
        assert fn(root, nodes[a], nodes[b]).val == want, (fn.__name__, a, b)
        assert fn(root, nodes[b], nodes[a]).val == want

chain, chain_nodes = bst_from_inserts(list(range(1, 200)))        # right-skewed
assert lowest_common_ancestor(chain, chain_nodes[50], chain_nodes[150]) is chain_nodes[50]

pair, pair_nodes = bst_from_inserts([5, 3])
assert lowest_common_ancestor(pair, pair_nodes[5], pair_nodes[3]) is pair

rng = random.Random(15)
for _ in range(200):
    vals = rng.sample(range(1000), rng.randint(2, 50))
    r, ns = bst_from_inserts(vals)
    a, b = rng.sample(vals, 2)
    assert lowest_common_ancestor(r, ns[a], ns[b]) is lca_general(r, ns[a], ns[b])
print("all BST LCA tests passed")

Edge cases and pitfalls

  • Strict comparisons. Use < and > so that the case “one target equals the current node” falls into the split branch and returns the node.
  • Assuming a balanced tree. State the complexity as O(h) and mention that it degrades to O(n) for a skewed BST.
  • Missing targets. The walk returns the split point even if one value is absent. If presence is not guaranteed, search for both values separately afterwards.
  • Using the general algorithm is correct but misses what the interviewer is testing: using the BST ordering.

Where this shows up in data engineering

The split-point walk is how range queries descend an ordered index: a B-tree search for a key range [low, high] follows a single path until the two bounds diverge, then branches. Knowing that is useful when you reason about why range scans on an indexed or sorted column are cheap and why they degrade when the structure is unbalanced.

By Data Career Hub Editorial · Last reviewed Oct 2026 · Python 3 solutions verified with assert-based tests

Progress is saved in this browser only. No account needed.

Search
Filter by type