1123. Lowest Common Ancestor of Deepest Leaves
Problem Description
You are given the root of a binary tree. Your task is to find the lowest common ancestor (LCA) of all the deepest leaves in the tree.
Let's break down the key concepts:
Leaf nodes: A node is a leaf if it has no children (neither left nor right child).
Depth: The depth of a node represents how far it is from the root. The root has depth 0
, its children have depth 1
, their children have depth 2
, and so on.
Deepest leaves: These are the leaf nodes that are farthest from the root (have the maximum depth among all leaves).
Lowest Common Ancestor (LCA): For a set of nodes, the LCA is the deepest node that has all the nodes in the set as descendants (including the node itself if it's in the set).
Your goal is to find the LCA specifically of all the deepest leaves. This means:
- First identify which leaves are at the maximum depth
- Then find the lowest (deepest) node that is an ancestor of all these deepest leaves
For example, if a tree has multiple leaves at depth 4
(and no leaves deeper than that), you need to find the deepest node that is an ancestor of all those depth-4 leaves. This could be:
- The root itself (if the deepest leaves are spread across different subtrees)
- An internal node (if all deepest leaves are within the same subtree)
- A deepest leaf itself (if there's only one deepest leaf)
The function should return the node that serves as the LCA of all deepest leaves.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure.
DFS
- Yes: According to the flowchart, when we have a tree structure, DFS is the recommended approach.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this problem.
This makes sense because:
- We need to explore the entire tree to find all the deepest leaves
- We need to track depth information as we traverse down the tree
- We need to compare depths from different subtrees to determine the LCA
- DFS allows us to process the tree recursively, computing depths bottom-up and determining the LCA based on the depths of left and right subtrees
The DFS approach perfectly aligns with the solution strategy where we recursively traverse the tree, calculate depths for each subtree, and determine the LCA based on comparing these depths. When the depths of left and right subtrees are equal, the current node is the LCA of the deepest leaves in its subtree.
Intuition
The key insight is that we need to find where all the deepest leaves "meet" as we go up the tree. Think of it like finding the lowest junction point where all the deepest paths converge.
When we traverse a tree using DFS, we can calculate the depth of each subtree from bottom to top. As we return from the recursive calls, we have information about the depths of left and right subtrees. This gives us three critical scenarios to consider:
-
Left subtree is deeper than right subtree: All the deepest leaves must be in the left subtree, so the LCA of deepest leaves is also in the left subtree. We can ignore the right subtree entirely.
-
Right subtree is deeper than left subtree: Similarly, all the deepest leaves must be in the right subtree, so the LCA is in the right subtree.
-
Both subtrees have the same depth: This is the interesting case! It means we have deepest leaves in both the left and right subtrees. The current node becomes the meeting point - it's the lowest node that has all these deepest leaves as descendants. Therefore, the current node is the LCA.
The beauty of this approach is that we're essentially "bubbling up" the answer from the bottom of the tree. Each node asks its children: "Who is the LCA of your deepest leaves, and how deep are they?" Based on the answers, it either passes up one of the children's LCAs (if one subtree is deeper) or declares itself as the new LCA (if both subtrees are equally deep).
This naturally handles all cases:
- If there's only one deepest leaf, we keep passing it up until we reach the root
- If all deepest leaves are in one subtree, we pass up that subtree's LCA
- If deepest leaves are spread across both subtrees of a node, that node becomes the LCA
The algorithm elegantly combines depth calculation with LCA finding in a single traversal, making it both efficient and intuitive.
Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.
Solution Approach
The solution implements a DFS approach using a helper function that returns two pieces of information for each node: the LCA candidate and the depth of the subtree.
Helper Function Design:
The dfs(root)
function returns a tuple (node, depth)
where:
node
is the potential LCA of the deepest leaves in this subtreedepth
is the maximum depth of leaves in this subtree
Base Case:
When we encounter a None
node (beyond a leaf), we return (None, 0)
. This serves as the foundation for our depth calculations.
Recursive Logic:
For each non-null node, we:
-
Recursively explore both subtrees:
- Call
dfs(root.left)
to get(l, d1)
- the LCA and depth from the left subtree - Call
dfs(root.right)
to get(r, d2)
- the LCA and depth from the right subtree
- Call
-
Compare depths and determine the LCA:
-
If
d1 > d2
: The left subtree is deeper, so all deepest leaves are on the left. We return(l, d1 + 1)
, passing up the left subtree's LCA and incrementing the depth. -
If
d1 < d2
: The right subtree is deeper, so all deepest leaves are on the right. We return(r, d2 + 1)
, passing up the right subtree's LCA and incrementing the depth. -
If
d1 == d2
: Both subtrees have the same depth, meaning deepest leaves exist in both subtrees. The current node is their LCA, so we return(root, d1 + 1)
.
-
Why increment depth by 1?
As we return from each recursive call, we're moving one level up the tree. The + 1
accounts for the edge between the current node and its child.
Final Result:
The main function calls dfs(root)
and extracts the first element of the tuple using [0]
, which gives us the LCA node.
Time and Space Complexity:
- Time:
O(n)
wheren
is the number of nodes, as we visit each node exactly once - Space:
O(h)
whereh
is the height of the tree, for the recursion stack
This elegant solution combines the depth calculation and LCA finding in a single pass, avoiding the need for multiple traversals or additional data structures to store leaf nodes and their depths.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate how the solution works.
Consider this binary tree:
1 / \ 2 3 / \ 4 5
We want to find the LCA of all deepest leaves. The deepest leaves are nodes 4 and 5 (both at depth 2).
Step-by-step DFS traversal:
- Start at root (1): Call
dfs(1)
- First, recursively call
dfs(2)
for the left subtree
- First, recursively call
- At node 2: Call
dfs(2)
- Recursively call
dfs(4)
for the left child
- Recursively call
- At node 4 (leaf): Call
dfs(4)
- Call
dfs(None)
for left child → returns(None, 0)
- Call
dfs(None)
for right child → returns(None, 0)
- Both depths are 0 (equal), so node 4 becomes the LCA
- Return
(4, 1)
to node 2
- Call
- Back at node 2: Continue with right child
- Call
dfs(5)
for the right child
- Call
- At node 5 (leaf): Call
dfs(5)
- Call
dfs(None)
for left child → returns(None, 0)
- Call
dfs(None)
for right child → returns(None, 0)
- Both depths are 0 (equal), so node 5 becomes the LCA
- Return
(5, 1)
to node 2
- Call
- Back at node 2: Process results from both children
- Left subtree returned
(4, 1)
- LCA is node 4, depth is 1 - Right subtree returned
(5, 1)
- LCA is node 5, depth is 1 - Depths are equal (1 == 1), so node 2 becomes the LCA of both deepest leaves
- Return
(2, 2)
to node 1
- Left subtree returned
- Back at root (1): Continue with right child
- Call
dfs(3)
for the right subtree
- Call
- At node 3 (leaf): Call
dfs(3)
- Call
dfs(None)
for left child → returns(None, 0)
- Call
dfs(None)
for right child → returns(None, 0)
- Both depths are 0 (equal), so node 3 becomes the LCA
- Return
(3, 1)
to node 1
- Call
- Back at root (1): Process results from both children
- Left subtree returned
(2, 2)
- LCA is node 2, depth is 2 - Right subtree returned
(3, 1)
- LCA is node 3, depth is 1 - Left is deeper (2 > 1), so we keep the left subtree's LCA
- Return
(2, 3)
- Left subtree returned
Final Result: The function returns node 2, which is indeed the LCA of the deepest leaves (nodes 4 and 5).
The key moment was at step 6, where node 2 recognized that it had deepest leaves in both its subtrees (both at depth 1 from node 2's perspective) and declared itself as their LCA.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import Optional, Tuple
9
10class Solution:
11 def lcaDeepestLeaves(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
12 """
13 Find the lowest common ancestor of the deepest leaves in a binary tree.
14
15 Args:
16 root: The root node of the binary tree
17
18 Returns:
19 The lowest common ancestor node of all deepest leaves
20 """
21
22 def dfs(node: Optional[TreeNode]) -> Tuple[Optional[TreeNode], int]:
23 """
24 Perform depth-first search to find the LCA of deepest leaves.
25
26 Args:
27 node: Current node being processed
28
29 Returns:
30 A tuple containing:
31 - The LCA of deepest leaves in the subtree rooted at node
32 - The depth of the deepest leaves in this subtree
33 """
34 # Base case: empty node has no LCA and depth 0
35 if node is None:
36 return None, 0
37
38 # Recursively process left and right subtrees
39 left_lca, left_depth = dfs(node.left)
40 right_lca, right_depth = dfs(node.right)
41
42 # If left subtree has deeper leaves, return its LCA
43 if left_depth > right_depth:
44 return left_lca, left_depth + 1
45
46 # If right subtree has deeper leaves, return its LCA
47 if left_depth < right_depth:
48 return right_lca, right_depth + 1
49
50 # If both subtrees have same depth, current node is the LCA
51 # This happens when deepest leaves exist in both subtrees
52 return node, left_depth + 1
53
54 # Return only the LCA node (first element of tuple)
55 return dfs(root)[0]
56
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 /**
18 * Finds the lowest common ancestor of the deepest leaves in a binary tree.
19 *
20 * @param root The root of the binary tree
21 * @return The lowest common ancestor node of all deepest leaves
22 */
23 public TreeNode lcaDeepestLeaves(TreeNode root) {
24 return findLcaWithDepth(root).getKey();
25 }
26
27 /**
28 * Recursively finds the LCA of deepest leaves and tracks the depth.
29 * Returns a pair containing the LCA node and the depth of deepest leaves in the subtree.
30 *
31 * @param node The current node being processed
32 * @return A Pair where the key is the LCA node and the value is the depth
33 */
34 private Pair<TreeNode, Integer> findLcaWithDepth(TreeNode node) {
35 // Base case: null node has depth 0
36 if (node == null) {
37 return new Pair<>(null, 0);
38 }
39
40 // Recursively process left and right subtrees
41 Pair<TreeNode, Integer> leftResult = findLcaWithDepth(node.left);
42 Pair<TreeNode, Integer> rightResult = findLcaWithDepth(node.right);
43
44 // Extract depths from the results
45 int leftDepth = leftResult.getValue();
46 int rightDepth = rightResult.getValue();
47
48 // If left subtree is deeper, the LCA is in the left subtree
49 if (leftDepth > rightDepth) {
50 return new Pair<>(leftResult.getKey(), leftDepth + 1);
51 }
52
53 // If right subtree is deeper, the LCA is in the right subtree
54 if (leftDepth < rightDepth) {
55 return new Pair<>(rightResult.getKey(), rightDepth + 1);
56 }
57
58 // If both subtrees have the same depth, current node is the LCA
59 return new Pair<>(node, leftDepth + 1);
60 }
61}
62
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 TreeNode* lcaDeepestLeaves(TreeNode* root) {
15 // Helper function to find LCA of deepest leaves
16 // Returns: pair<LCA node, depth from current node>
17 auto findLCAWithDepth = [&](this auto&& findLCAWithDepth, TreeNode* node) -> pair<TreeNode*, int> {
18 // Base case: null node has depth 0
19 if (!node) {
20 return {nullptr, 0};
21 }
22
23 // Recursively process left and right subtrees
24 auto [leftLCA, leftDepth] = findLCAWithDepth(node->left);
25 auto [rightLCA, rightDepth] = findLCAWithDepth(node->right);
26
27 // If left subtree is deeper, LCA is in left subtree
28 if (leftDepth > rightDepth) {
29 return {leftLCA, leftDepth + 1};
30 }
31
32 // If right subtree is deeper, LCA is in right subtree
33 if (leftDepth < rightDepth) {
34 return {rightLCA, rightDepth + 1};
35 }
36
37 // If both subtrees have same depth, current node is the LCA
38 return {node, leftDepth + 1};
39 };
40
41 // Return the LCA node from the result pair
42 return findLCAWithDepth(root).first;
43 }
44};
45
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Finds the lowest common ancestor of the deepest leaves in a binary tree.
17 * @param root - The root of the binary tree
18 * @returns The lowest common ancestor node of all deepest leaves
19 */
20function lcaDeepestLeaves(root: TreeNode | null): TreeNode | null {
21 /**
22 * Performs depth-first search to find the LCA of deepest leaves.
23 * @param node - Current node being processed
24 * @returns A tuple containing [LCA node of deepest leaves in subtree, depth of deepest leaves]
25 */
26 const findLCAWithDepth = (node: TreeNode | null): [TreeNode | null, number] => {
27 // Base case: null node has depth 0
28 if (node === null) {
29 return [null, 0];
30 }
31
32 // Recursively process left and right subtrees
33 const [leftLCA, leftDepth] = findLCAWithDepth(node.left);
34 const [rightLCA, rightDepth] = findLCAWithDepth(node.right);
35
36 // If left subtree has deeper leaves, return its LCA
37 if (leftDepth > rightDepth) {
38 return [leftLCA, leftDepth + 1];
39 }
40
41 // If right subtree has deeper leaves, return its LCA
42 if (leftDepth < rightDepth) {
43 return [rightLCA, rightDepth + 1];
44 }
45
46 // If both subtrees have same depth, current node is the LCA
47 return [node, leftDepth + 1];
48 };
49
50 // Return only the LCA node (first element of the tuple)
51 return findLCAWithDepth(root)[0];
52}
53
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. At each node, the function performs constant time operations (comparing depths d1
and d2
, and returning values). Since we visit all n
nodes exactly once and perform O(1)
work at each node, the overall time complexity is O(n)
, where n
is the number of nodes in the binary tree.
Space Complexity: O(n)
The space complexity is determined by the recursive call stack. In the worst case, the binary tree is completely unbalanced (e.g., a skewed tree where each node has only one child), resulting in a recursion depth equal to the number of nodes n
. This would require O(n)
space on the call stack. In the best case of a perfectly balanced tree, the recursion depth would be O(log n)
, but since we consider worst-case complexity, the space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding When to Return Current Node as LCA
The Mistake: A common error is incorrectly determining when the current node should be the LCA. Developers might think that any node with two non-null children should be considered as a potential LCA, leading to code like:
def dfs(node):
if node is None:
return None, 0
left_lca, left_depth = dfs(node.left)
right_lca, right_depth = dfs(node.right)
# WRONG: Just checking if both children exist
if node.left and node.right:
return node, max(left_depth, right_depth) + 1
elif left_depth > right_depth:
return left_lca, left_depth + 1
else:
return right_lca, right_depth + 1
Why It's Wrong: The current node should be the LCA only when deepest leaves exist in BOTH subtrees at the SAME depth. Simply having two children doesn't guarantee that deepest leaves are in both subtrees.
The Fix:
Always check if left_depth == right_depth
to determine if deepest leaves are distributed across both subtrees:
if left_depth == right_depth: return node, left_depth + 1
Pitfall 2: Incorrect Depth Calculation for Leaf Nodes
The Mistake: Some might try to handle leaf nodes as a special case and assign them depth 1 immediately:
def dfs(node):
if node is None:
return None, 0
# WRONG: Treating leaf nodes specially
if node.left is None and node.right is None:
return node, 1
left_lca, left_depth = dfs(node.left)
right_lca, right_depth = dfs(node.right)
# ... rest of logic
Why It's Wrong:
This breaks the consistent depth calculation. When a leaf node's children (which are None
) return depth 0, adding 1 gives the correct depth of 1 for the leaf. Special-casing leaves can lead to off-by-one errors and makes the logic more complex.
The Fix:
Let the recursive logic handle all nodes uniformly. The base case of None
returning depth 0 naturally produces the correct depths for all nodes:
# For a leaf node: # dfs(None) returns (None, 0) for both children # max(0, 0) + 1 = 1, which is correct for a leaf
Pitfall 3: Forgetting to Handle Single-Node Trees
The Mistake: Not considering that a tree might consist of only a root node (which is also a leaf):
def lcaDeepestLeaves(self, root):
if not root.left and not root.right:
# Might forget this case
pass
return dfs(root)[0]
Why It's Wrong: A single-node tree is a valid input where the root is the only leaf and thus the LCA of all deepest leaves (itself).
The Fix:
The recursive solution naturally handles this case. When the root has no children, both recursive calls return (None, 0)
, and since depths are equal, the root itself is returned as the LCA:
# For root with no children: # left_lca, left_depth = None, 0 # right_lca, right_depth = None, 0 # Since 0 == 0, return (root, 1)
Pitfall 4: Confusion About What to Return
The Mistake: Returning the depth instead of the node, or forgetting to extract the node from the tuple:
def lcaDeepestLeaves(self, root):
# WRONG: Returning the entire tuple
return dfs(root)
# OR WRONG: Returning the depth
return dfs(root)[1]
Why It's Wrong: The problem asks for the LCA node itself, not the depth or the tuple containing both.
The Fix: Always extract the first element (the node) from the returned tuple:
return dfs(root)[0]
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!