Lowest Common Ancestor of Deepest Leaves
MedPrompt
Given the root of a binary tree, return the lowest common ancestor of its deepest leaves.
Recall that:
- The node of a binary tree is a leaf if and only if it has no children
- The depth of the root of the tree is
0. if the depth of a node isd, the depth of each of its children isd + 1. - The lowest common ancestor of a set
Sof nodes, is the nodeAwith the largest depth such that every node inSis in the subtree with rootA.
Example 1:
Input: root = [3,5,1,6,2,0,8,null,null,7,4]
Output: [2,7,4]
Explanation: We return the node with value 2, colored in yellow in the diagram.
The nodes coloured in blue are the deepest leaf-nodes of the tree.
Note that nodes 6, 0, and 8 are also leaf nodes, but the depth of them is 2, but the depth of nodes 7 and 4 is 3.Example 2:
Input: root = [1]
Output: [1]
Explanation: The root is the deepest node in the tree, and it's the lca of itself.Example 3:
Input: root = [0,1,3,null,2]
Output: [2]
Explanation: The deepest leaf node in the tree is 2, the lca of one node is itself.
Constraints:
- The number of nodes in the tree will be in the range
[1, 1000]. 0 <= Node.val <= 1000- The values of the nodes in the tree are unique.
Note: This question is the same as 865: https://leetcode.com/problems/smallest-subtree-with-all-the-deepest-nodes/
Approaches
2 approaches with complexity analysis and trade-offs.
This approach tackles the problem by breaking it into two distinct, more straightforward subproblems. First, it performs a full traversal of the tree to identify all the leaf nodes that reside at the maximum possible depth. After collecting these leaves, it initiates a second traversal to find their lowest common ancestor (LCA).
Algorithm
- Pass 1: Find Deepest Leaves
- Initialize
maxDepth = -1and an empty listdeepestLeaves. - Define a recursive helper function
findDeepestLeaves(node, depth). - In the helper, if the node is a leaf, compare its
depthwithmaxDepth. Ifdepth > maxDepth, clear the list and add the current leaf. Ifdepth == maxDepth, just add the current leaf. - Traverse the entire tree by calling
findDeepestLeaves(root, 0).
- Initialize
- Pass 2: Find LCA of Deepest Leaves
- If the
deepestLeaveslist contains only one node, return that node as it's the LCA of itself. - Create a
Setof thedeepestLeavesfor efficient O(1) lookups. - Define another recursive helper function
findLCA(node, targets). - In this function, if the current
nodeis null or is one of the target leaves, return thenode. - Recursively call
findLCAfor the left and right children. - If the calls for both left and right subtrees return a non-null node, it means deepest leaves were found on both sides, making the current
nodethe LCA. Return the currentnode. - Otherwise, if only one side returned a non-null node, propagate that result up the recursion chain.
- The initial call
findLCA(root, targets)will return the final answer.
- If the
Walkthrough
This method involves two main passes over the tree.
Pass 1: Identify the Deepest Leaves First, we need to determine the maximum depth of the tree and find all leaf nodes at that depth. This can be done with a single Depth-First Search (DFS) traversal.
- We maintain a global variable
maxDepthand a listdeepestLeaves. - We traverse the tree with a function
findDeepestLeaves(node, depth). - When we encounter a leaf node (
node.left == null && node.right == null), we check itsdepth.- If
depth > maxDepth, we've found a new deepest level. We updatemaxDepth, clear ourdeepestLeaveslist, and add the current leaf. - If
depth == maxDepth, we've found another leaf at the current maximum depth, so we add it to the list.
- If
Pass 2: Find the Lowest Common Ancestor (LCA) Once we have the set of deepest leaves, we find their LCA.
- If there's only one deepest leaf, it is its own LCA.
- Otherwise, we use a standard LCA-finding algorithm. A recursive function
findLCA(node, targets)is suitable. We convert our list of leaves to aSetfor efficient lookups. - This function returns the
nodeitself if it's one of the targets. It recursively checks its left and right subtrees. If it gets non-null results from both children, it means targets exist in both subtrees, making the currentnodethe LCA. If only one child returns a result, that result is propagated up.
import java.util.*; class Solution { private int maxDepth = -1; private List<TreeNode> deepestLeaves = new ArrayList<>(); public TreeNode lcaDeepestLeaves(TreeNode root) { // Pass 1: Find the deepest leaves findDeepestLeaves(root, 0); // If only one, it's the LCA if (deepestLeaves.size() == 1) { return deepestLeaves.get(0); } // Pass 2: Find the LCA of the collected leaves Set<TreeNode> targets = new HashSet<>(deepestLeaves); return findLCA(root, targets); } private void findDeepestLeaves(TreeNode node, int depth) { if (node == null) { return; } if (node.left == null && node.right == null) { // It's a leaf if (depth > maxDepth) { maxDepth = depth; deepestLeaves.clear(); deepestLeaves.add(node); } else if (depth == maxDepth) { deepestLeaves.add(node); } return; } findDeepestLeaves(node.left, depth + 1); findDeepestLeaves(node.right, depth + 1); } private TreeNode findLCA(TreeNode node, Set<TreeNode> targets) { if (node == null || targets.contains(node)) { return node; } TreeNode left = findLCA(node.left, targets); TreeNode right = findLCA(node.right, targets); if (left != null && right != null) { return node; } return left != null ? left : right; }}Complexity
Time
O(N). The first pass to find deepest leaves visits every node once, taking O(N) time. The second pass to find the LCA also traverses the tree, taking another O(N) time. The total time complexity is O(N) + O(N) = O(N).
Space
O(N). The `deepestLeaves` list can store up to N/2 nodes in the case of a complete binary tree. The recursion stack for both DFS traversals can also reach a depth of H (the tree height), which is O(N) in the worst-case (a skewed tree).
Trade-offs
Pros
The logic is straightforward as it separates the problem into two well-known tree algorithms: finding nodes at a certain level and finding the LCA of a set of nodes.
Cons
Requires two separate traversals of the tree, making it less efficient than a single-pass solution.
Needs extra space to store all the deepest leaf nodes, which can be up to O(N) for a complete binary tree.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * public int val; * public TreeNode left; * public TreeNode right; * public TreeNode(int val = 0, TreeNode left = null, TreeNode right = null) { * this.val = val; * this.left = left; * this.right = right; * } * } */ public class Solution { public TreeNode LcaDeepestLeaves ( TreeNode root ) { ( TreeNode , int ) Dfs ( TreeNode root ) { if ( root == null ) { return ( null , 0 ); } var l = Dfs ( root . left ); var r = Dfs ( root . right ); int d1 = l . Item2 ; int d2 = r . Item2 ; if ( d1 > d2 ) { return ( l . Item1 , d1 + 1 ); } if ( d1 < d2 ) { return ( r . Item1 , d2 + 1 ); } return ( root , d1 + 1 ); } return Dfs ( root ). Item1 ; } }Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.