Introduction & Problem Explanation
The Lowest Common Ancestor (LCA) of two nodes p and q in a binary
tree is defined as the lowest node in the tree that has both p and q as descendants
(where we allow a node to be a descendant of itself).
For example, in a tree with root 3, and child nodes p = 5 and q = 1,
the LCA is the root 3 itself because both nodes diverge from it. If p = 5 and
q = 4 (where 4 is a child of 5), the LCA is 5. This algorithm is widely used in
relational databases, organization chart software, and inheritance mapping compilers.
Imagine a company org chart, which is structured like a tree. You have two employees, Alice (p) and Bob (q). You want to find their Lowest Common Ancestor, which represents their nearest shared manager who has authority over both of them. If Alice and Bob work in completely different departments (e.g., Engineering and Marketing), their nearest common manager is the CEO at the top split point. However, if Alice is a Senior Engineer and Bob is a Junior Engineer reporting directly to Alice, then Alice herself is their nearest common manager, as she has authority over Bob and is a descendant of herself in the org chart hierarchy!
The Algorithmic Approach
1. Post-Order Recursive DFS Search (O(n) Time, O(h) Space)
We traverse the tree recursively. For any given node:
- If the current node is
null, or equals eitherporq, we return the current node. This represents finding one of our target nodes or hitting a leaf boundary. - We recursively search the left subtree:
TreeNode left = lowestCommonAncestor(root.left, p, q). - We recursively search the right subtree:
TreeNode right = lowestCommonAncestor(root.right, p, q). - If both
leftandrightsearch results are non-null, it means one target node was found in the left subtree and the other was found in the right subtree. Therefore, the current node is their split point, which makes it the LCA. - If only one of them is non-null, it means both target nodes reside in that non-null subtree, so we pass that result up to the parent caller.
O(n) time complexity. The space complexity is
O(h) due to the recursion call stack.
Step-by-Step Execution Walkthrough
Let's trace the recursive LCA algorithm on a tree with root 3, left child 5, right
child 1. We want to find the LCA of p = 5 and q = 1:
- Step 1 (Root call): Start at root node
3.- We call
lowestCommonAncestor(3, 5, 1). - Since 3 is not null, and doesn't equal p (5) or q (1), we proceed to search left and right.
- We call
- Step 2 (Search Left Subtree of 3): We call
lowestCommonAncestor(3.left, 5, 1)which islowestCommonAncestor(5, 5, 1).- Since the node is
5, which matchesp = 5, the base case triggers! - The method immediately returns node
5to the root caller.
- Since the node is
- Step 3 (Search Right Subtree of 3): We call
lowestCommonAncestor(3.right, 5, 1)which islowestCommonAncestor(1, 5, 1).- Since the node is
1, which matchesq = 1, the base case triggers! - The method immediately returns node
1to the root caller.
- Since the node is
- Step 4 (Resolve at Root 3):
- We receive
left = 5andright = 1. - Since both
leftandrightare non-null, the split point condition matches:if (left != null && right != null) return root;. - The root node
3is returned as the LCA.
- We receive
- Step 5 (Completion): The algorithm completes and correctly returns node
3.
Key Code Snippets & Explanations
Here is why the main logic in the solution is important:
if (root == null || root == p || root == q) return root;: The recursive base case. It stops the search and returns the node as soon as we find eitherporq, or hit a leaf.if (left != null && right != null) return root;: The core split condition. If both subtrees returned a match, the current node is the lowest common ancestor of the two targets.return left != null ? left : right;: If only one subtree returned a match, it means both nodes are on that side, so we bubble that match up.
Java Implementation Code
Below is the complete, self-contained Java source code that solves this problem. It also includes a
main method that traces the execution with console outputs.
package io.practise.dsa;
public class LowestCommonAncestor {
public static class TreeNode {
public int val;
public TreeNode left, right;
public TreeNode(int val) { this.val = val; }
}
// Recursive DFS - O(n)
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;
}
public static void main(String[] args) {
LowestCommonAncestor solver = new LowestCommonAncestor();
TreeNode root = new TreeNode(3);
TreeNode p = new TreeNode(5);
TreeNode q = new TreeNode(1);
root.left = p;
root.right = q;
System.out.println("--- Lowest Common Ancestor Demonstration ---");
TreeNode lca = solver.lowestCommonAncestor(root, p, q);
System.out.println("LCA Val: " + lca.val);
}
}
Conclusion
Solving the Lowest Common Ancestor (LCA) problem highlights how we can leverage key data structures and algorithmic paradigms (like two pointers, hash maps, or dynamic programming) to significantly optimize runtime and simplify code complexity.