Lowest Common Ancestor in a BST
in the wild Find the most specific shared category of two products in a taxonomy tree, or the closest junction joining two waypoints in a routing tree.
Problem
Given the root of a BST and two values p and q known to exist in it, return the lowest common ancestor (LCA) node — the deepest node that has both p and q in its subtree. A node may be its own ancestor.
Examples
6
/ \
2 8
/ \ / \
0 4 7 9
/ \
3 5
LCA(2, 8) = 6 (split happens at the root)
LCA(2, 4) = 2 (2 is an ancestor of 4)
LCA(3, 5) = 4
LCA(0, 5) = 2
Why this is the canonical "exploit the BST ordering" problem
In a generic binary tree, finding LCA is O(n): you walk both subtrees and merge information up. In a BST, the ordering turns it into O(h):
- If both
pandqare less than the current node → both live in the left subtree → descend left. - If both are greater than the current node → descend right.
- Otherwise (split — one ≤ node, one ≥ node, or current node is one of them) → this is the LCA.
You walk a single root-to-LCA path. No backtracking, no auxiliary structure, no second pass. The BST property lets you "feel" where the split happens just by comparing.
This is the cleanest example of a theme that runs through every BST algorithm: "the ordering is your map — don't waste it". The same idea lets BST insert/search/min/max all be O(h), versus O(n) on an unordered binary tree.
Hints
Hint 1 — when must the LCA be the current node?
The LCA is the first (highest-up) node that separates p and q into different subtrees. If both p and q are on the same side of the current node, you can safely descend that side without losing the LCA.
Hint 2 — the split test, written tightly
Let lo = min(p, q) and hi = max(p, q). The LCA is the first node whose value lies in [lo, hi] — anything strictly less goes right; strictly greater goes left.
Hint 3 — iterate, no recursion needed
The recursion is tail-recursive — you only descend, never combine. A simple while loop with node = node.left / node = node.right is cleaner and uses O(1) extra space.
Solution
from dataclasses import dataclass
from typing import Optional
@dataclass
class Node:
val: int
left: Optional["Node"] = None
right: Optional["Node"] = None
def lca_bst(root: Optional[Node], p: int, q: int) -> Optional[Node]:
lo, hi = min(p, q), max(p, q)
n = root
while n is not None:
if n.val < lo: n = n.right # both descendants are larger
elif n.val > hi: n = n.left # both descendants are smaller
else: return n # the split — this is the LCA
return None
# Build the example tree.
root = Node(6,
Node(2, Node(0), Node(4, Node(3), Node(5))),
Node(8, Node(7), Node(9)))
cases = [(2, 8, 6), (2, 4, 2), (3, 5, 4), (0, 5, 2)]
for p, q, expected in cases:
got = lca_bst(root, p, q).val
mark = "✓" if got == expected else "✗"
print(f"{mark} lca_bst(.., {p}, {q}) = {got} (expected {expected})")class Node {
constructor(val, left = null, right = null) {
this.val = val; this.left = left; this.right = right;
}
}
function lcaBst(root, p, q) {
const lo = Math.min(p, q), hi = Math.max(p, q);
let n = root;
while (n !== null) {
if (n.val < lo) n = n.right;
else if (n.val > hi) n = n.left;
else return n;
}
return null;
}
const root = new Node(6,
new Node(2, new Node(0), new Node(4, new Node(3), new Node(5))),
new Node(8, new Node(7), new Node(9)));
const cases = [[2, 8, 6], [2, 4, 2], [3, 5, 4], [0, 5, 2]];
for (const [p, q, expected] of cases) {
const got = lcaBst(root, p, q).val;
console.log(`${got === expected ? "✓" : "✗"} lcaBst(.., ${p}, ${q}) = ${got} (expected ${expected})`);
}pub struct Node {
pub val: i32,
pub left: Option<Box<Node>>,
pub right: Option<Box<Node>>,
}
pub fn lca_bst<'a>(root: &'a Option<Box<Node>>, p: i32, q: i32) -> Option<&'a Node> {
let (lo, hi) = (p.min(q), p.max(q));
let mut cur = root.as_deref();
while let Some(n) = cur {
if n.val < lo { cur = n.right.as_deref(); }
else if n.val > hi { cur = n.left.as_deref(); }
else { return Some(n); }
}
None
}
fn leaf(v: i32) -> Option<Box<Node>> {
Some(Box::new(Node { val: v, left: None, right: None }))
}
fn node(v: i32, l: Option<Box<Node>>, r: Option<Box<Node>>) -> Option<Box<Node>> {
Some(Box::new(Node { val: v, left: l, right: r }))
}
fn main() {
let root = node(6,
node(2, leaf(0), node(4, leaf(3), leaf(5))),
node(8, leaf(7), leaf(9)));
let cases = [(2, 8, 6), (2, 4, 2), (3, 5, 4), (0, 5, 2)];
for (p, q, expected) in cases {
let got = lca_bst(&root, p, q).unwrap().val;
let mark = if got == expected { "✓" } else { "✗" };
println!("{mark} lca_bst(.., {}, {}) = {} (expected {})", p, q, got, expected);
}
}#include <algorithm>
#include <iostream>
struct Node {
int val;
Node* left;
Node* right;
Node(int v, Node* l = nullptr, Node* r = nullptr) : val(v), left(l), right(r) {}
};
Node* lca_bst(Node* root, int p, int q) {
int lo = std::min(p, q), hi = std::max(p, q);
Node* n = root;
while (n) {
if (n->val < lo) n = n->right;
else if (n->val > hi) n = n->left;
else return n;
}
return nullptr;
}
int main() {
Node* root = new Node(6,
new Node(2, new Node(0), new Node(4, new Node(3), new Node(5))),
new Node(8, new Node(7), new Node(9)));
int cases[][3] = { {2, 8, 6}, {2, 4, 2}, {3, 5, 4}, {0, 5, 2} };
for (auto& c : cases) {
int got = lca_bst(root, c[0], c[1])->val;
std::cout << (got == c[2] ? "✓" : "✗")
<< " lca_bst(.., " << c[0] << ", " << c[1] << ") = " << got
<< " (expected " << c[2] << ")\n";
}
}#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int val;
struct Node* left;
struct Node* right;
} Node;
static int imin(int a, int b) { return a < b ? a : b; }
static int imax(int a, int b) { return a > b ? a : b; }
Node* lca_bst(Node* root, int p, int q) {
int lo = imin(p, q), hi = imax(p, q);
Node* n = root;
while (n) {
if (n->val < lo) n = n->right;
else if (n->val > hi) n = n->left;
else return n;
}
return NULL;
}
static Node* mk(int v, Node* l, Node* r) {
Node* n = malloc(sizeof(Node));
n->val = v; n->left = l; n->right = r;
return n;
}
int main(void) {
Node* root = mk(6,
mk(2, mk(0, NULL, NULL), mk(4, mk(3, NULL, NULL), mk(5, NULL, NULL))),
mk(8, mk(7, NULL, NULL), mk(9, NULL, NULL)));
int cases[][3] = { {2, 8, 6}, {2, 4, 2}, {3, 5, 4}, {0, 5, 2} };
for (int i = 0; i < 4; i++) {
int got = lca_bst(root, cases[i][0], cases[i][1])->val;
printf("%s lca_bst(.., %d, %d) = %d (expected %d)\n",
got == cases[i][2] ? "✓" : "✗",
cases[i][0], cases[i][1], got, cases[i][2]);
}
return 0;
}The whole problem collapses to "walk down until you straddle the values". Notice how every implementation is essentially the same six lines.
Complexity
- Time
- O(h) — one root-to-LCA descent. In a balanced BST that's
O(log n); in a degenerate one,O(n). - Space
- O(1) — iterative version uses no stack. The recursive version uses
O(h)stack. - vs general tree LCA
- Without the BST ordering, you need an
O(n)post-order pass (or pre-process withO(n)Euler tour + RMQ forO(1)per query). The BST givesO(log n)per query, no preprocessing.
In the wild
- Taxonomy / category trees sorted by ID — finding the lowest shared category for two products is exactly LCA.
- Routing trees. A multicast routing tree's LCA of two destinations is the branching point packets diverge at.
- Range queries on sorted indices. "The smallest enclosing block for a key range" is conceptually an LCA on a sorted-key tree (used by segment trees + interval trees).
Variations
- LCA in a general (non-BST) binary tree. Different algorithm — recurse into both children; if both return non-null, the current node is the LCA; otherwise propagate the non-null one up.
O(n). - LCA when nodes might not exist. First validate both keys are present (one search each), then run LCA.
- Repeated queries. Pre-process the tree once with an Euler tour + sparse-table RMQ; subsequent queries are
O(1)(butO(n log n)preprocessing). - Distance between two nodes in a BST.
depth(p) + depth(q) - 2 * depth(LCA(p, q)).