Trees · Binary Tree Fundamentals

Binary Tree LCA Search

Finding the deepest node that has both target nodes as descendants by having each recursive call report upward whether it found either target, and letting the first node where both reports come back non-null claim itself as the answer.

  • 6 min read
  • 11 practice questions
  • 1 graded problem
On this page
  1. Description
  2. The approach
  3. A second flavor: comparing two root-to-node paths
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

The lowest common ancestor (LCA) of two nodes is the deepest node that has both of them in its subtree. It's the point where their paths from the root split. One recursive pass finds it. Each subtree reports whether it found a target, and the first node where both sides report one is the answer. A second approach records each node's path from the root and compares the two.

We'll go from simple to more technical:

  1. The pattern: one pass, each side reports up.
  2. A second flavor: compare the two root-to-node paths.
  3. Where "closest shared parent" questions appear in production.
  4. The assumption the one-pass version depends on - that both nodes exist.

The approach

Ask every subtree one question: "did you find p or q?" A subtree with neither returns null. A node that is p or q returns itself right away. Every other node passes up whatever its children found. The first node to hear back from both sides is where the two targets split: the LCA.

6

2

9

1

4

3

5

11

6

2

9

1

4

3

5

11

Finding the LCA of 1 and 5:

NodeLeft reportsRight reportsReturns
1itself: it's a target
3nothingnothingnull
5itself: it's a target
4null55, passed up
2152: both sides found one, so 2 is the LCA
9nothingnothing (11 isn't a target)null
62null2, passed up

Java

TreeNode find(TreeNode node, int p, int q) {
    if (node == null || node.val == p || node.val == q) return node;
    TreeNode left = find(node.left, p, q);
    TreeNode right = find(node.right, p, q);
    if (left != null && right != null) return node;   // found one on each side - this IS the LCA
    return (left != null) ? left : right;              // otherwise, pass up whichever side found something
}

Python

def find(node: TreeNode | None, p: int, q: int) -> TreeNode | None:
    if node is None or node.val in (p, q):
        return node
    left = find(node.left, p, q)
    right = find(node.right, p, q)
    if left is not None and right is not None:
        return node                          # found one on each side - this IS the LCA
    return left if left is not None else right   # otherwise, pass up whichever side found something

Returning a target without searching inside it is safe when both targets are guaranteed to exist. If the other target is below it, the found node is the LCA anyway, since a node counts as its own ancestor. For 4 and 5 above, the answer is 4.

A second flavor: comparing two root-to-node paths

A more explicit way: build each target's path from the root (the search from Binary Tree DFS Traversal Orders), then walk both paths from the front. The last place they still agree is the LCA. For 1 and 5, the paths are 6, 2, 1 and 6, 2, 4, 5. They agree on 6 and 2, so the LCA is 2.

Java

List<Integer> pathToP = rootToNodePath(root, p);   // reuse the root-to-node-path technique
List<Integer> pathToQ = rootToNodePath(root, q);
int lca = pathToP.get(0);                            // the root, guaranteed to be common to both
for (int i = 0; i < Math.min(pathToP.size(), pathToQ.size()); i++) {
    if (!pathToP.get(i).equals(pathToQ.get(i))) break;
    lca = pathToP.get(i);
}
return lca;

Python

def lca_by_paths(root: TreeNode, p: int, q: int) -> int:
    path_to_p = root_to_node_path(root, p)   # reuse the root-to-node-path technique
    path_to_q = root_to_node_path(root, q)
    lca = path_to_p[0]                       # the root, guaranteed to be common to both
    for a, b in zip(path_to_p, path_to_q):
        if a != b:
            break
        lca = a
    return lca

This is easier to trust at a glance but does more work: two searches plus a comparison, against one pass.

Where you'll see it

The closest shared folder of two files is an LCA. So is the most specific category two products share in a store's catalog tree. Both ask where two paths from the root stop agreeing.

Which tool for which job?

You needUse
One LCA query, both nodes guaranteed presentthe one-pass recursion
One query, a node might be missingthe recursion without early return, counting found targets
Nodes with parent pointersequalize depths, then walk up together
Many queries on the same treebinary lifting (O(log n) per query)
LCA in a BSTwalk down by comparing values (no recursion over both sides)

Common mistakes

MistakeWhat happensDo this instead
Trusting the result when a target may be missingreturns the target that was foundcount both targets, or check existence first
Searching inside a found targetwasted workreturn as soon as a target is found
Comparing by value with duplicate valuesthe wrong node matchescompare node references
One full search per query, many queriesO(n) eachpreprocess: binary lifting
Comparing paths built by copying listsO(n h) memorypush and pop one path

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. What does the one-pass version return if q isn't in the tree at all?

Show answer

Most likely the node for p, with no warning. The algorithm assumes both targets exist and has no way to check. If a target might be missing, count the targets found, or check that both exist first.

2. Why is the first node to hear back from both sides the lowest common ancestor, not just some ancestor?

Show answer

Higher ancestors also contain both targets, but there both targets sit on the same side, so only one child reports back. The node where the targets are on different sides is the deepest one that still has both. That's the definition of the lowest common ancestor.

3. Why is the LCA the last place the two root-to-node paths agree, not the first?

Show answer

Every shared ancestor appears in both paths, in the same order, from the root down. The paths only differ once the targets head into different children. The last shared entry, just before that split, is the deepest common ancestor.

Key takeaways

  • The node where both recursive calls find something is the lowest common ancestor.
  • Returning a target early is safe only when both targets are sure to exist.
  • Comparing two root-to-node paths finds the same node: the last place they agree.
  • Both approaches answer one question: where do the paths to the two targets split?

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You needUse
One query, both presentone-pass recursion
A target may be missingfull search, count found
Parent pointersequalize depths, walk up
Many queriesbinary lifting
BSTwalk down by value

Snippets worth knowing by heart

Java

TreeNode lca(TreeNode node, TreeNode p, TreeNode q) {
    if (node == null || node == p || node == q) return node;   // safe only if both exist
    TreeNode left = lca(node.left, p, q), right = lca(node.right, p, q);
    if (left != null && right != null) return node;            // split here
    return left != null ? left : right;
}

Python

def lca_with_parents(a, b):
    def depth(n):
        d = 0
        while n.parent:
            n, d = n.parent, d + 1
        return d
    da, db = depth(a), depth(b)
    while da > db: a, da = a.parent, da - 1
    while db > da: b, db = b.parent, db - 1
    while a is not b:
        a, b = a.parent, b.parent
    return a

The traps

  • A missing target: the search returns the one it found as the LCA. Count both, or check existence first.
  • Searching inside a found target: it wastes work. Return as soon as a target is found.
  • Value comparisons with duplicates: the wrong node matches. Compare node references.
  • O(n) per query: many queries make it slow. Preprocess with binary lifting.
  • Copying paths: building path lists costs O(n h) memory. Push and pop one path.

Staff-level points, one line each

  • Missing target: plain version said 4, checked version said None.
  • Parent pointers: LCA(7, 4) = 2, LCA(5, 4) = 5, LCA(6, 8) = 3.
  • Binary lifting: 500 queries matched, 9.8 jumps on average (depth up to 899).
  • dist(a, b) = depth(a) + depth(b) - 2 depth(LCA).

Say it in an interview

  • "Each subtree reports whether it found a target; the first node with reports from both sides is the LCA."
  • "Returning early at a target relies on both targets being in the tree."
  • "With parent pointers I'd equalize depths and walk up together."
  • "For many queries I'd precompute binary lifting tables."

Practice

This skill comes with 11 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Binary Tree LCA Search free

Lowest Common Ancestor of a Binary Tree

MediumProCoding problemJava · Python

Given a binary tree and two of its values p and q (each appears exactly once), return their lowest common ancestor: the deepest node that has both p and q in its subtree, where a node counts as being in its own subtree.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Binary Tree LCA Search free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime