Skip to content
intermediatePhase 4 · Trees & Heaps

Lowest Common Ancestor

Solve LCA problems using recursion and tree properties.

45m
4 problems
Topic Progress0%

LCA Concepts and Approaches

The Lowest Common Ancestor (LCA) of two nodes p and q is the deepest node that has both p and q as descendants (a node can be a descendant of itself).

Node Definition

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
}

Visual Example

        3
       / \
      5   1
     / \\ / \
    6  2 0  8
      / \
     7   4
  • LCA(5, 1) = 3
  • LCA(5, 4) = 5 (node is ancestor of itself)
  • LCA(6, 4) = 5
  • LCA(7, 4) = 2

LCA for Binary Trees - DFS Approach

Key insight: If current node is null, p, or q, return it. Otherwise recurse left and right. If both sides return non-null, current is LCA. If one side is null, return the other.

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;

    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);

    if (left != null && right != null) return root;
    return left != null ? left : right;
}

Why this works:

  1. If p and q are in different subtrees, the current node is their LCA
  2. If one node is ancestor of the other, the ancestor is returned first
  3. If neither exists in a subtree, null propagates up

LCA for BST - Exploit BST Property

Since BST is ordered, we can determine which subtree to search:

public TreeNode lcaBST(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null) return null;

    if (p.val < root.val && q.val < root.val) {
        return lcaBST(root.left, p, q);  // Both in left
    }
    if (p.val > root.val && q.val > root.val) {
        return lcaBST(root.right, p, q);  // Both in right
    }
    return root;  // Split point = LCA
}

LCA with Parent Pointers

Given each node has a parent pointer, find LCA by moving both to same depth, then moving up together:

public TreeNode lcaWithParent(TreeNode p, TreeNode q) {
    int depthP = getDepth(p);
    int depthQ = getDepth(q);

    // Move deeper node up
    while (depthP > depthQ) { p = p.parent; depthP--; }
    while (depthQ > depthP) { q = q.parent; depthQ--; }

    // Move both up until they meet
    while (p != q) {
        p = p.parent;
        q = q.parent;
    }
    return p;
}

private int getDepth(TreeNode node) {
    int depth = 0;
    while (node != null) {
        node = node.parent;
        depth++;
    }
    return depth;
}

Complexity Analysis

Approach Time Space
Binary Tree DFS O(n) O(h)
BST O(h) O(h)
Parent Pointers O(h) O(1)

where h is tree height (log n for balanced, n for skewed)

LCA Variants and Applications

LCA of Multiple Nodes

Generalize to find LCA of a list of nodes:

public TreeNode lcaMultiple(TreeNode root, List<TreeNode> nodes) {
    Set<TreeNode> nodeSet = new HashSet<>(nodes);
    return helper(root, nodeSet);
}

private TreeNode helper(TreeNode root, Set<TreeNode> nodes) {
    if (root == null) return null;
    if (nodes.contains(root)) return root;

    TreeNode left = helper(root.left, nodes);
    TreeNode right = helper(root.right, nodes);

    if (left != null && right != null) return root;
    return left != null ? left : right;
}

Distance Between Two Nodes

Distance = depth(p) + depth(q) - 2 * depth(LCA)

public int findDistance(TreeNode root, TreeNode p, TreeNode q) {
    TreeNode lca = lowestCommonAncestor(root, p, q);
    return findDepth(lca, p, 0) + findDepth(lca, q, 0);
}

private int findDepth(TreeNode root, TreeNode target, int depth) {
    if (root == null) return -1;
    if (root == target) return depth;
    int left = findDepth(root.left, target, depth + 1);
    if (left != -1) return left;
    return findDepth(root.right, target, depth + 1);
}

Path Between Two Nodes

LCA helps find path: path(p, q) = path(root, p) + path(root, q) - 2 * path(root, LCA)

public List<TreeNode> findPath(TreeNode root, TreeNode target) {
    List<TreeNode> path = new ArrayList<>();
    findPathHelper(root, target, path);
    return path;
}

private boolean findPathHelper(TreeNode root, TreeNode target, List<TreeNode> path) {
    if (root == null) return false;
    path.add(root);
    if (root == target) return true;
    if (findPathHelper(root.left, target, path) || findPathHelper(root.right, target, path)) {
        return true;
    }
    path.remove(path.size() - 1);  // Backtrack
    return false;
}

Binary Tree Diameter

Diameter = longest path between any two nodes. Uses LCA concept:

private int diameter = 0;

public int diameterOfBinaryTree(TreeNode root) {
    height(root);
    return diameter;
}

private int height(TreeNode node) {
    if (node == null) return 0;
    int left = height(node.left);
    int right = height(node.right);
    diameter = Math.max(diameter, left + right);
    return 1 + Math.max(left, right);
}

Key Pattern

The LCA pattern applies whenever you need to find the "meeting point" of two paths in a tree. This includes:

  • Distance between nodes
  • Path between nodes
  • Merging information from two subtrees
  • Any problem asking about relationships between two nodes

Practice Problems

0/1solved
Lowest Common Ancestor of a Binary Tree
Recursive DFS

Given a binary tree, find the lowest common ancestor (LCA) of two given nodes p and q. The LCA is the deepest node that has both p and q as descendants.

Example:

Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1

Output: 3

Node 3 is the deepest node that has both 5 and 1 as descendants.

Optimal Solution — O(n) time, O(h) where h is tree height space

Recursive DFS: return non-null from subtrees. If both sides return non-null, current is LCA. Otherwise return the non-null side.

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null || root == p || root == q) return root;
        
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        
        if (left != null && right != null) return root;
        return left != null ? left : right;
    }
}

Edge Cases:

  • One node is ancestor of the other → return the ancestor
  • Both nodes are in the left subtree
  • Both nodes are in the right subtree
  • Nodes are in different subtrees of root → return root
  • Tree with only 2 nodes

Quiz

1. In the LCA DFS algorithm, what happens when both left and right recursive calls return non-null?

Question 1 options

2. What is the time complexity of finding LCA in a binary tree?

Question 2 options

3. What is the primary purpose of Lowest Common Ancestor?

Question 3 options

4. What is a common mistake when implementing Lowest Common Ancestor?

Question 4 options

Flashcards

Question

What is the key insight for LCA in a binary tree?

Answer

Recurse left and right. If both return non-null, current node is LCA. If one is null, return the other. If current is p or q, return it immediately.

Question

How does LCA differ between BST and binary tree?

Answer

BST: compare values to decide which subtree (O(h)). Binary tree: must explore both subtrees (O(n)) since no ordering property.

Question

What is Lowest Common Ancestor?

Answer

Lowest Common Ancestor is a key concept in software engineering.

Question

When to use Lowest Common Ancestor?

Answer

Use Lowest Common Ancestor when building production systems that require reliability, scalability, and maintainability.

Question

Lowest Common Ancestor best practices

Answer

Follow SOLID principles, write clean code, test thoroughly, document decisions, and monitor in production.

Revision Notes

Key Takeaways

  • 1.LCA DFS works by returning non-null from subtrees and checking both sides
  • 2.BST LCA exploits ordering for O(h) time vs O(n) for general trees
  • 3.Distance between nodes = depth(p) + depth(q) - 2*depth(LCA)
  • 4.The pattern generalizes to finding common ancestors of multiple nodes

Interview Tips

  • Start with the simple recursive insight: both non-null means current is LCA
  • For BST, mention the O(h) optimization using ordering property
  • Know how to extend LCA to distance and path problems
  • Clarify if parent pointers are available - changes the approach

Cheat Sheet

Lowest Common Ancestor Cheat Sheet

Binary Tree LCA:

public TreeNode lca(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null || root == p || root == q) return root;
    TreeNode left = lca(root.left, p, q);
    TreeNode right = lca(root.right, p, q);
    if (left != null && right != null) return root;
    return left != null ? left : right;
}

BST LCA (O(h))):

public TreeNode lcaBST(TreeNode root, TreeNode p, TreeNode q) {
    if (p.val < root.val && q.val < root.val) return lcaBST(root.left, p, q);
    if (p.val > root.val && q.val > root.val) return lcaBST(root.right, p, q);
    return root;
}

Distance Formula: distance(p, q) = depth(p) + depth(q) - 2 × depth(LCA)

Key Patterns:

  • Both non-null → current is LCA
  • One non-null → return that one
  • Both null → return null
  • Node matches p or q → return that node

Applications:

  • Distance between nodes
  • Path between nodes
  • Binary tree diameter
  • Any "meeting point" problem