Given a binary search tree and two values a and b that both appear in it, find their lowest common ancestor — the deepest node that has both a and b somewhere beneath it (a node counts as a descendant of itself). Think of a company org chart sorted by employee ID: the lowest common ancestor of two people is the most junior manager whose reporting chain still contains both of them. You return that node's value.
A binary search tree keeps every value to the left of a node smaller than the node, and every value to the right larger. You will use that ordering to walk straight to the answer without exploring the whole tree.
// Each node is a plain object. Leaves have left === null and right === null.
type TreeNode = { val: number; left: TreeNode | null; right: TreeNode | null };
// root is never null; a and b are distinct values, both present in the tree.
// Returns the VALUE (a number) of the lowest common ancestor node.
function binarySearchTreeLowestCommonAncestor(
root: TreeNode,
a: number,
b: number,
): number;
Consider this tree:
6
/ \
2 8
/ \ / \
0 4 7 9
/ \
3 5
// a and b sit on opposite sides of the root — 6 is the split point.
binarySearchTreeLowestCommonAncestor(root, 2, 8); // -> 6
// 2 is an ancestor of 4, so the answer is 2 itself.
// Both values live in the left subtree; the walk turns left, then stops at 2.
binarySearchTreeLowestCommonAncestor(root, 2, 4); // -> 2
a and b are guaranteed to exist in the tree, so you never have to handle a missing value.a is an ancestor of b, the lowest common ancestor can be a itself.(root, a, b) and (root, b, a) returns the same value.val (a number), not the node object.You'll find the deepest node that owns both a and b as descendants, using the binary-search-tree ordering to walk straight there in one downward pass.
You have two values that both live in a binary search tree, and you want the lowest node whose subtree still contains both of them. That node is where the path from the root to a and the path from the root to b finally split apart — above it the two paths share every step, below it they go separate ways. Picture a family tree sorted by age, or an org chart sorted by employee ID: the lowest common ancestor of two people is the most junior manager who still has both of them somewhere underneath.
The phrase "lowest common ancestor" packs two ideas. Common ancestor — a node that has both a and b beneath it (and a node counts as beneath itself). Lowest — of all such ancestors, the one furthest from the root.
Here is the key observation. In a binary search tree, everything smaller than a node sits in its left subtree and everything larger sits in its right subtree. So when you stand on a node and look at your two targets, exactly one of three things is true:
a and b are smaller than node.val. Then both live in the left subtree, and so does their lowest common ancestor. Move left.a and b are larger than node.val. Then both live in the right subtree. Move right.<= node.val and the other is >= node.val. Then this is the first node where the two paths diverge. This node is the lowest common ancestor. Stop.Notice the "straddle" case also covers a value being its own ancestor: if a equals node.val, then a is not strictly less and not strictly greater, so the targets straddle and we stop right here — exactly what we want, since a is then an ancestor of b.
If you have seen lowest-common-ancestor for a general binary tree — one with no ordering — you'd reach for the standard recursion: look in both subtrees, and the node where the two targets first appear in different subtrees is the answer.
function lcaGeneral(node, a, b) {
if (node === null) return null;
if (node.val === a || node.val === b) return node; // found one target here
const left = lcaGeneral(node.left, a, b);
const right = lcaGeneral(node.right, a, b);
// a came from one side and b from the other -> this node is the split.
if (left !== null && right !== null) return node;
// Otherwise both targets (if any) are on the same side; bubble it up.
return left !== null ? left : right;
}
function binarySearchTreeLowestCommonAncestor(root, a, b) {
return lcaGeneral(root, a, b).val;
}
This is correct — it passes every test. But it throws away the one fact that makes this a search tree. It recurses into both children at every node, so in the worst case it touches all n nodes and uses recursion stack proportional to the tree's height. We searched the whole tree to find something the ordering could have pointed us to directly. For a tree of a million sorted values, the general version may visit a million nodes; the ordering can find the answer in about twenty steps.
Use the ordering. From the root, apply the three-way comparison and step down once per node. No recursion, no second subtree, no extra memory.
function binarySearchTreeLowestCommonAncestor(root, a, b) {
let node = root;
while (node !== null) {
if (a < node.val && b < node.val) {
// Both targets are smaller, so both live in the left subtree.
node = node.left;
} else if (a > node.val && b > node.val) {
// Both targets are larger, so both live in the right subtree.
node = node.right;
} else {
// The targets straddle this node (or one equals it): this is the
// first place the paths to a and b diverge — the lowest ancestor.
return node.val;
}
}
}
module.exports = { binarySearchTreeLowestCommonAncestor };
The shift from the naive version is that we never look at both children. Each comparison tells us which single direction still contains both targets, so we discard the other half of the tree and keep descending. The moment the targets fall on opposite sides of node — or one of them is node — we've reached the split point and return its value. Because we follow one path down, the work is the tree's height h: O(h) time and O(1) extra space. The while loop will always hit the else branch before node becomes null, because both values are guaranteed present, so the split point must exist.
Take the sample tree and call binarySearchTreeLowestCommonAncestor(root, 3, 5).
node = 6. Is 3 < 6 and 5 < 6? Yes — both are smaller. Move left: node = 2.node = 2. Is 3 < 2 and 5 < 2? No. Is 3 > 2 and 5 > 2? Yes — both are larger. Move right: node = 4.node = 4. Is 3 < 4 and 5 < 4? No (5 is not). Is 3 > 4 and 5 > 4? No (3 is not). They straddle 4. Return 4.Three nodes visited in a tree of nine. Now trace the ancestor case, binarySearchTreeLowestCommonAncestor(root, 2, 4). At 6, both 2 and 4 are smaller, so move left to 2. At 2, is 2 < 2? No. Is 2 > 2? No. The targets straddle (one of them is 2), so we return 2 immediately — the ancestor is its own answer.
O(n) into O(h). The shortcut is the point of the question.else branch, when the targets fall on opposite sides of the node. A common bug is treating "I found a" as the stop condition and continuing to search for b; you don't need to, because the straddle test already detects an ancestor.< on one side but <= on the other — keep both comparisons strict (a < node.val && b < node.val). If neither "both smaller" nor "both larger" is true, you straddle and stop. Mixing strict and non-strict can make a node equal to a target slip into the wrong branch and walk past its own answer.a is an ancestor of b, the answer is a. The straddle branch handles this for free, since a is neither strictly less nor strictly greater than itself.a and b, so (root, a, b) and (root, b, a) give the same result. If your code only works when a < b, you've added an assumption the problem doesn't make.a and b exist, which is why the loop can return without a fallback. If you generalize to possibly-absent values, you'd need to verify each is actually in the tree first, or the walk could return a node that isn't a true common ancestor.O(n). That's the version interviewers ask once they've seen you do the BST shortcut.a and b instead of downward from the root: collect a's ancestors into a set, then climb from b until you hit one. No tree-wide search, no ordering required.min and max against each node, since any node that splits the extremes splits everything between them.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Given a binary search tree and two values a and b that both appear in it, find their lowest common ancestor — the deepest node that has both a and b somewhere beneath it (a node counts as a descendant of itself). Think of a company org chart sorted by employee ID: the lowest common ancestor of two people is the most junior manager whose reporting chain still contains both of them. You return that node's value.
A binary search tree keeps every value to the left of a node smaller than the node, and every value to the right larger. You will use that ordering to walk straight to the answer without exploring the whole tree.
// Each node is a plain object. Leaves have left === null and right === null.
type TreeNode = { val: number; left: TreeNode | null; right: TreeNode | null };
// root is never null; a and b are distinct values, both present in the tree.
// Returns the VALUE (a number) of the lowest common ancestor node.
function binarySearchTreeLowestCommonAncestor(
root: TreeNode,
a: number,
b: number,
): number;
Consider this tree:
6
/ \
2 8
/ \ / \
0 4 7 9
/ \
3 5
// a and b sit on opposite sides of the root — 6 is the split point.
binarySearchTreeLowestCommonAncestor(root, 2, 8); // -> 6
// 2 is an ancestor of 4, so the answer is 2 itself.
// Both values live in the left subtree; the walk turns left, then stops at 2.
binarySearchTreeLowestCommonAncestor(root, 2, 4); // -> 2
a and b are guaranteed to exist in the tree, so you never have to handle a missing value.a is an ancestor of b, the lowest common ancestor can be a itself.(root, a, b) and (root, b, a) returns the same value.val (a number), not the node object.You'll find the deepest node that owns both a and b as descendants, using the binary-search-tree ordering to walk straight there in one downward pass.
You have two values that both live in a binary search tree, and you want the lowest node whose subtree still contains both of them. That node is where the path from the root to a and the path from the root to b finally split apart — above it the two paths share every step, below it they go separate ways. Picture a family tree sorted by age, or an org chart sorted by employee ID: the lowest common ancestor of two people is the most junior manager who still has both of them somewhere underneath.
The phrase "lowest common ancestor" packs two ideas. Common ancestor — a node that has both a and b beneath it (and a node counts as beneath itself). Lowest — of all such ancestors, the one furthest from the root.
Here is the key observation. In a binary search tree, everything smaller than a node sits in its left subtree and everything larger sits in its right subtree. So when you stand on a node and look at your two targets, exactly one of three things is true:
a and b are smaller than node.val. Then both live in the left subtree, and so does their lowest common ancestor. Move left.a and b are larger than node.val. Then both live in the right subtree. Move right.<= node.val and the other is >= node.val. Then this is the first node where the two paths diverge. This node is the lowest common ancestor. Stop.Notice the "straddle" case also covers a value being its own ancestor: if a equals node.val, then a is not strictly less and not strictly greater, so the targets straddle and we stop right here — exactly what we want, since a is then an ancestor of b.
If you have seen lowest-common-ancestor for a general binary tree — one with no ordering — you'd reach for the standard recursion: look in both subtrees, and the node where the two targets first appear in different subtrees is the answer.
function lcaGeneral(node, a, b) {
if (node === null) return null;
if (node.val === a || node.val === b) return node; // found one target here
const left = lcaGeneral(node.left, a, b);
const right = lcaGeneral(node.right, a, b);
// a came from one side and b from the other -> this node is the split.
if (left !== null && right !== null) return node;
// Otherwise both targets (if any) are on the same side; bubble it up.
return left !== null ? left : right;
}
function binarySearchTreeLowestCommonAncestor(root, a, b) {
return lcaGeneral(root, a, b).val;
}
This is correct — it passes every test. But it throws away the one fact that makes this a search tree. It recurses into both children at every node, so in the worst case it touches all n nodes and uses recursion stack proportional to the tree's height. We searched the whole tree to find something the ordering could have pointed us to directly. For a tree of a million sorted values, the general version may visit a million nodes; the ordering can find the answer in about twenty steps.
Use the ordering. From the root, apply the three-way comparison and step down once per node. No recursion, no second subtree, no extra memory.
function binarySearchTreeLowestCommonAncestor(root, a, b) {
let node = root;
while (node !== null) {
if (a < node.val && b < node.val) {
// Both targets are smaller, so both live in the left subtree.
node = node.left;
} else if (a > node.val && b > node.val) {
// Both targets are larger, so both live in the right subtree.
node = node.right;
} else {
// The targets straddle this node (or one equals it): this is the
// first place the paths to a and b diverge — the lowest ancestor.
return node.val;
}
}
}
module.exports = { binarySearchTreeLowestCommonAncestor };
The shift from the naive version is that we never look at both children. Each comparison tells us which single direction still contains both targets, so we discard the other half of the tree and keep descending. The moment the targets fall on opposite sides of node — or one of them is node — we've reached the split point and return its value. Because we follow one path down, the work is the tree's height h: O(h) time and O(1) extra space. The while loop will always hit the else branch before node becomes null, because both values are guaranteed present, so the split point must exist.
Take the sample tree and call binarySearchTreeLowestCommonAncestor(root, 3, 5).
node = 6. Is 3 < 6 and 5 < 6? Yes — both are smaller. Move left: node = 2.node = 2. Is 3 < 2 and 5 < 2? No. Is 3 > 2 and 5 > 2? Yes — both are larger. Move right: node = 4.node = 4. Is 3 < 4 and 5 < 4? No (5 is not). Is 3 > 4 and 5 > 4? No (3 is not). They straddle 4. Return 4.Three nodes visited in a tree of nine. Now trace the ancestor case, binarySearchTreeLowestCommonAncestor(root, 2, 4). At 6, both 2 and 4 are smaller, so move left to 2. At 2, is 2 < 2? No. Is 2 > 2? No. The targets straddle (one of them is 2), so we return 2 immediately — the ancestor is its own answer.
O(n) into O(h). The shortcut is the point of the question.else branch, when the targets fall on opposite sides of the node. A common bug is treating "I found a" as the stop condition and continuing to search for b; you don't need to, because the straddle test already detects an ancestor.< on one side but <= on the other — keep both comparisons strict (a < node.val && b < node.val). If neither "both smaller" nor "both larger" is true, you straddle and stop. Mixing strict and non-strict can make a node equal to a target slip into the wrong branch and walk past its own answer.a is an ancestor of b, the answer is a. The straddle branch handles this for free, since a is neither strictly less nor strictly greater than itself.a and b, so (root, a, b) and (root, b, a) give the same result. If your code only works when a < b, you've added an assumption the problem doesn't make.a and b exist, which is why the loop can return without a fallback. If you generalize to possibly-absent values, you'd need to verify each is actually in the tree first, or the walk could return a node that isn't a true common ancestor.O(n). That's the version interviewers ask once they've seen you do the BST shortcut.a and b instead of downward from the root: collect a's ancestors into a set, then climb from b until you hit one. No tree-wide search, no ordering required.min and max against each node, since any node that splits the extremes splits everything between them.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.