With no BST ordering to guide you, find the lowest common ancestor by a single post-order DFS: each call returns p, q, or null upward, and the first node that hears back from both subtrees is the answer.
Given the root of a binary tree (not a BST) and two distinct nodes p and q, return their lowest common ancestor — the deepest node that has both as descendants (a node is a descendant of itself). For the tree [3,5,1,6,2,0,8,null,null,7,4] with p=5, q=1 the answer is 3; with p=5, q=4 it is 5 (an ancestor of itself, since 4 lives beneath 5).
p or q is found in that subtree (else null). At each node, look at what your left and right calls returned: if both came back non-null, p and q are split across this node, so this node is the LCA. Otherwise bubble up whichever single side was non-null.null, return null — nothing here.p or q. If this node isa target, return it immediately. You don't need to keep searching below it — a node is its own descendant, so it can be the ancestor.left = lca(node.left) and right = lca(node.right). This is the post-order step: you decide after the children answer.left and right are both non-null, the targets straddle this node → return node. Otherwise return the one non-null side (or null if neither found anything).p is an ancestor of q. It doesn't: when the recursion reaches p, it returns p up without descending, and since q is somewhere beneath it, the higher ancestors only ever see one non-null side — so the answer stays p. This relies on the problem's guarantee that both p and q exist in the tree.In the worst case you visit every node once, so time is O(n). The only extra memory is the recursion stack, which is as deep as the tree's height h — O(log n) for a balanced tree, O(n) for a degenerate one. You cannot do better than O(n) here because, lacking ordering, any node could be the answer until proven otherwise.
p and q split, in O(h) time and O(1)space — no recursion needed. Here, with no order, the "ask both children, then merge" post-order shape is the same backbone used by Diameter of a Binary Tree, Maximum Path Sum, and Balanced Binary Tree: each returns a small summary upward and the parent combines them.p=5 and q=1. Each call returns p, q, or null upward; the first node that hears back from both sides is the LCA.1class TreeNode {2 val: number;3 left: TreeNode | null;4 right: TreeNode | null;5 constructor(val: number) {6 this.val = val;7 this.left = null;8 this.right = null;9 }10}1112function lowestCommonAncestor(13 root: TreeNode | null,14 p: TreeNode,15 q: TreeNode,16): TreeNode | null {17 if (root === null) return null; // fell off the tree18 if (root === p || root === q) return root; // found p or q -> bubble it up19▶20▶ const left = lowestCommonAncestor(root.left, p, q);21 const right = lowestCommonAncestor(root.right, p, q);2223 if (left && right) return root; // p and q on opposite sides -> LCA24 return left ?? right; // both on one side (or neither)25}
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val: number) {
this.val = val;
this.left = null;
this.right = null;
}
}
function lowestCommonAncestor(
root: TreeNode | null,
p: TreeNode,
q: TreeNode,
): TreeNode | null {
if (root === null) return null; // fell off the tree
if (root === p || root === q) return root; // found p or q -> bubble it up
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left && right) return root; // p and q on opposite sides -> LCA
return left ?? right; // both on one side (or neither)
}null node contributes nothing, so return null. This terminates the recursion below the leaves.root === p || root === q, return root at once. We compare by node identity, not value, and stop descending — a node is its own descendant, so it can still be the LCA if the other target lies beneath it.left and right are both non-null, the two targets are on opposite sides of this node, so this node is the LCA — return it. Otherwise exactly one side (or neither) found a target, so bubble that side up with left ?? right.TreeNode objects; comparing references avoids ambiguity if values were ever duplicated.class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val: number) {
this.val = val;
this.left = null;
this.right = null;
}
}
function lowestCommonAncestor(
root: TreeNode | null,
p: TreeNode,
q: TreeNode,
): TreeNode | null {
if (root === null) return null; // fell off the tree
if (root === p || root === q) return root; // found p or q -> bubble it up
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left && right) return root; // p and q on opposite sides -> LCA
return left ?? right; // both on one side (or neither)
}Record the full path from the root to p and to q, then walk both lists together and return the last node they share — conceptually simple, but it stores two paths and may traverse twice.
function lowestCommonAncestor(
root: TreeNode | null,
p: TreeNode,
q: TreeNode,
): TreeNode | null {
function findPath(target: TreeNode): TreeNode[] | null {
const path: TreeNode[] = [];
function dfs(node: TreeNode | null): boolean {
if (!node) return false;
path.push(node);
if (node === target) return true;
if (dfs(node.left) || dfs(node.right)) return true;
path.pop();
return false;
}
return dfs(root) ? path : null;
}
const pathP = findPath(p);
const pathQ = findPath(q);
if (!pathP || !pathQ) return null;
let lca: TreeNode | null = null;
for (let i = 0; i < pathP.length && i < pathQ.length; i++) {
if (pathP[i] === pathQ[i]) lca = pathP[i];
else break;
}
return lca;
}O(n) extra space) and can scan the tree twice. The one-pass post-order solution decides on the way back up with only O(h) stack space.| LCA in a plain binary tree (no BST order) | post-order DFS, return p/q/null |
| node is p or q | return it immediately (a node is its own descendant) |
| left and right both non-null | this node is the LCA |
| only one side non-null | bubble that side up (left ?? right) |
function lowestCommonAncestor(root, p, q) {
if (!root || root === p || root === q) return root;
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left && right) return root; // split -> LCA
return left ?? right; // bubble up the one side
}p or q if found in that subtree, the LCA if both were found below, otherwise null.