Lesson 99 of 164
Lowest Common Ancestor of a Binary Tree
Given a binary tree with unique values and two values p and q that are both in the tree, return the lowest common ancestor (LCA): the deepest node that has both p and q as descendants. A node counts as a descendant of itself.
LeetCode: 236. Lowest Common Ancestor of a Binary Tree
Example
graph TB
a3["3"] --> a5["5"]
a3 --> a1["1"]
a5 --> a6["6"]
a5 --> a2["2"]
a1 --> a0["0"]
a1 --> a8["8"]
a2 --> a7["7"]
a2 --> a4["4"]lca(5, 1) = 3
lca(5, 4) = 5 (5 is an ancestor of 4, and of itself)
lca(6, 4) = 5This tree is not a BST, so the BST shortcut of comparing values with the node doesn't apply.
Brute force: compare the two root paths
Find the path from the root to p and the path from the root to q. Both start at the root, and they share nodes until they split. The last shared node is the LCA.
path(6) = 3 -> 5 -> 6
path(4) = 3 -> 5 -> 2 -> 4
^ last shared nodeA tree node is an object with an int val and two reference fields, left and right. A missing child is null, and the empty tree is a null root. OptionalInt is a container that holds either one int or nothing, so a method can report "no answer" without returning null.
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
public class Main {
static class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
static TreeNode node(int val, TreeNode left, TreeNode right) {
return new TreeNode(val, left, right);
}
static TreeNode leaf(int val) {
return new TreeNode(val, null, null);
}
// Adds the values from root to target to path; returns false if target is not found.
static boolean findPath(TreeNode root, int target, List<Integer> path) {
if (root == null) return false;
path.add(root.val);
if (root.val == target || findPath(root.left, target, path) || findPath(root.right, target, path)) {
return true;
}
path.remove(path.size() - 1);
return false;
}
static OptionalInt lca(TreeNode root, int p, int q) {
List<Integer> pathP = new ArrayList<>();
List<Integer> pathQ = new ArrayList<>();
if (!findPath(root, p, pathP) || !findPath(root, q, pathQ)) {
return OptionalInt.empty();
}
int i = 0;
while (i < pathP.size() && i < pathQ.size() && pathP.get(i).equals(pathQ.get(i))) {
i++;
}
return OptionalInt.of(pathP.get(i - 1)); // the root is always shared, so i >= 1
}
public static void main(String[] args) {
TreeNode root = node(3, node(5, leaf(6), node(2, leaf(7), leaf(4))), node(1, leaf(0), leaf(8)));
System.out.println(lca(root, 5, 1) + " " + lca(root, 5, 4) + " " + lca(root, 6, 4));
System.out.println(lca(root, 5, 99)); // 99 is not in the tree
}
}OptionalInt[3] OptionalInt[5] OptionalInt[5]
OptionalInt.emptyThe comparison uses equals because List<Integer> holds boxed Integer objects, and == on them compares references.
Time: O(n): at most two traversals. Space: O(h) for the paths and the recursion.
Optimal: one post-order recursion
Search for both values at once. At each node:
- If the node is
porq, return it: the LCA is this node or above it. - Otherwise, search both subtrees. If both return something,
pandqare on different sides, so this node is the LCA. If only one side returns something, pass it up.
import java.util.OptionalInt;
public class Main {
static class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
static TreeNode node(int val, TreeNode left, TreeNode right) {
return new TreeNode(val, left, right);
}
static TreeNode leaf(int val) {
return new TreeNode(val, null, null);
}
static OptionalInt lca(TreeNode root, int p, int q) {
if (root == null) return OptionalInt.empty();
if (root.val == p || root.val == q) return OptionalInt.of(root.val);
OptionalInt left = lca(root.left, p, q);
OptionalInt right = lca(root.right, p, q);
if (left.isPresent() && right.isPresent()) return OptionalInt.of(root.val);
return left.isPresent() ? left : right;
}
public static void main(String[] args) {
TreeNode root = node(3, node(5, leaf(6), node(2, leaf(7), leaf(4))), node(1, leaf(0), leaf(8)));
System.out.println(lca(root, 5, 1) + " " + lca(root, 5, 4) + " " + lca(root, 6, 4));
System.out.println(lca(root, 5, 99)); // 99 is not in the tree: the result is wrong
}
}OptionalInt[3] OptionalInt[5] OptionalInt[5]
OptionalInt[5]Time: O(n): one traversal. Space: O(h) for the recursion.
Returning early at p is safe because both values are guaranteed to be in the tree: if q is below p, then p is the answer; if not, the other side of some ancestor will find q.
Edge cases
- One value is an ancestor of the other: that value is returned.
- A value that is not in the tree: the optimal version returns the other value, which is wrong, as the last line of the example shows. If that can happen, use the path version, which returns an empty result, or count how many of the two values were found during the recursion.
p == q: the node itself.
