872. Leaf-Similar Trees
Problem Description
You are given two binary trees and need to determine if they are "leaf-similar". Two binary trees are considered leaf-similar when their leaf value sequences are identical.
A leaf value sequence is formed by collecting all the leaf node values from a binary tree in left-to-right order. A leaf node is a node that has no children (both left and right children are null).
For example, if a binary tree has leaf nodes with values 6, 7, 4, 9, 8
when traversed from left to right, then its leaf value sequence is (6, 7, 4, 9, 8)
.
Your task is to:
- Find the leaf value sequence of the first tree (
root1
) - Find the leaf value sequence of the second tree (
root2
) - Compare these sequences and return
true
if they are exactly the same,false
otherwise
The solution uses a depth-first search (DFS) approach to traverse each tree and collect leaf values. The helper function dfs
recursively explores the tree:
- When it encounters a leaf node (identified by
root.left == root.right
, meaning both children areNone
), it adds the node's value to the list - Otherwise, it recursively traverses the left subtree (if it exists) and then the right subtree (if it exists)
After collecting leaf sequences l1
and l2
from both trees, the solution simply compares these lists for equality using l1 == l2
.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: Although this is specifically a binary tree problem, a tree is a special type of graph (an acyclic connected graph). Binary trees are graphs with specific properties where each node has at most two children.
Is it a tree?
- Yes: The problem explicitly deals with binary trees. We need to traverse two binary trees and compare their leaf node sequences.
DFS
- Yes: This is our conclusion. For tree problems, DFS is a natural choice, especially when we need to:
- Traverse all nodes in the tree
- Reach leaf nodes (nodes at the deepest level)
- Process nodes in a specific order (left-to-right for leaves)
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this tree traversal problem.
The DFS pattern is ideal here because:
- We need to explore each path from root to leaf completely before moving to the next path
- DFS naturally visits leaves in left-to-right order when we process left subtrees before right subtrees
- The recursive nature of DFS matches the recursive structure of trees perfectly
- We can easily identify and collect leaf nodes during the traversal when we encounter nodes with no children
Intuition
When we need to compare if two trees have the same leaf sequence, the key insight is that we need to extract and compare just the leaf nodes from each tree, ignoring their internal structure.
Think of it like comparing two family trees - we only care if they have the same descendants at the bottom generation, not how the family branches are arranged. The internal nodes (non-leaf nodes) and the tree structure don't matter; only the left-to-right order of leaves matters.
To collect leaves in left-to-right order, we need a traversal strategy that:
- Visits every node to find all leaves
- Maintains the left-to-right ordering
DFS is perfect for this because when we recursively visit the left subtree before the right subtree, we naturally encounter leaves in left-to-right order. It's like reading a book - we go deep into the left side first, record any leaves we find, then move to the right.
The elegant observation in the solution is identifying a leaf node: a node is a leaf when root.left == root.right
(both are None
). This is a concise way of saying "this node has no children."
Once we have both leaf sequences, comparing them is straightforward - just check if the two lists are equal. If l1 == l2
, the trees are leaf-similar.
The beauty of this approach is its simplicity: instead of trying to compare trees structurally while traversing, we separate the problem into two independent steps:
- Extract leaf sequences from each tree
- Compare the sequences
This decomposition makes the solution clean and easy to understand.
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The solution implements a DFS traversal to collect leaf values from both trees, then compares the resulting sequences.
Implementation Details:
-
Helper Function Design: We create a
dfs
helper function that takes two parameters:root
: The current node being visitednums
: A list that accumulates leaf values during traversal
-
Leaf Node Detection: The key insight is the condition
root.left == root.right
. This checks if both children areNone
(sinceNone == None
isTrue
), elegantly identifying leaf nodes without explicitly checkingroot.left is None and root.right is None
. -
DFS Traversal Pattern: The function follows a standard recursive DFS pattern:
if root.left: dfs(root.left, nums) if root.right: dfs(root.right, nums)
By visiting the left subtree before the right subtree, we ensure leaves are collected in left-to-right order.
-
List as Accumulator: We use Python lists
l1
andl2
to accumulate leaf values. The lists are passed by reference to the recursive function, so all recursive calls append to the same list. -
Main Logic Flow:
- Initialize two empty lists:
l1 = []
andl2 = []
- Call
dfs(root1, l1)
to populatel1
with leaves from the first tree - Call
dfs(root2, l2)
to populatel2
with leaves from the second tree - Return
l1 == l2
to check if the sequences are identical
- Initialize two empty lists:
Time Complexity: O(n + m)
where n
and m
are the number of nodes in the two trees respectively. We visit each node exactly once.
Space Complexity: O(h1 + h2)
for the recursive call stack (where h1
and h2
are the heights of the trees), plus O(l1 + l2)
for storing the leaf values (where l1
and l2
are the number of leaves in each tree).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with two small binary trees to see how the algorithm determines if they're leaf-similar.
Example Trees:
Tree 1: Tree 2: 3 5 / \ / \ 5 1 6 1 / \ \ 6 2 2
Step 1: Extract leaves from Tree 1
Starting with root1
(value 3), we call dfs(root1, l1)
where l1 = []
:
- At node 3: Not a leaf (has children), so traverse left to node 5
- At node 5: Not a leaf (has children), so traverse left to node 6
- At node 6: This is a leaf! (
left == right == None
), add 6 tol1
:l1 = [6]
- Back to node 5, traverse right to node 2
- At node 2: This is a leaf! Add 2 to
l1
:l1 = [6, 2]
- Back to node 3, traverse right to node 1
- At node 1: This is a leaf! Add 1 to
l1
:l1 = [6, 2, 1]
Step 2: Extract leaves from Tree 2
Starting with root2
(value 5), we call dfs(root2, l2)
where l2 = []
:
- At node 5: Not a leaf (has children), so traverse left to node 6
- At node 6: This is a leaf! Add 6 to
l2
:l2 = [6]
- Back to node 5, traverse right to node 1
- At node 1: Not a leaf (has right child), skip left, traverse right to node 2
- At node 2: This is a leaf! Add 2 to
l2
:l2 = [6, 2]
- Back to node 1: No more children to visit
- Back to node 5: No more children to visit
Wait, we have a problem! Tree 2's leaf sequence is [6, 2]
, but we need to check node 1 more carefully. Node 1 has only a right child (node 2), so it's not a leaf. Let me correct the traversal:
Actually, the leaf sequence for Tree 2 is just [6, 2]
because node 1 is not a leaf (it has a right child).
Step 3: Compare sequences
- Tree 1 leaf sequence:
[6, 2, 1]
- Tree 2 leaf sequence:
[6, 2]
- Compare:
[6, 2, 1] == [6, 2]
returnsFalse
These trees are not leaf-similar because their leaf sequences differ.
Example of Leaf-Similar Trees:
Let's modify Tree 2 to make them leaf-similar:
Tree 1: Tree 2: 3 7 / \ / \ 5 1 6 4 / \ / / \ 6 2 2 1 8 \ 1
For Tree 2's new structure:
- Traverse to node 7 → left to node 6 → left to node 2 (leaf):
[2]
- Back to node 6 (done) → back to node 7 → right to node 4
- At node 4 → left to node 1 (leaf):
[2, 1]
- Back to node 4 → right to node 8 → right to node 1 (leaf):
[2, 1, 1]
Wait, this doesn't match. Let me create a simpler matching example:
Tree 1: Tree 2: 1 2 / \ \ 6 2 1 / \ 6 2
Tree 1 leaves: Visit left (6 is leaf), then right (2 is leaf) = [6, 2]
Tree 2 leaves: Visit right to node 1, then left (6 is leaf), then right (2 is leaf) = [6, 2]
Both sequences are [6, 2]
, so these trees are leaf-similar despite having completely different structures!
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
8class Solution:
9 def leafSimilar(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> bool:
10 """
11 Check if two binary trees have the same leaf value sequence.
12
13 Args:
14 root1: Root of the first binary tree
15 root2: Root of the second binary tree
16
17 Returns:
18 True if both trees have the same leaf value sequence, False otherwise
19 """
20
21 def collect_leaves(node: Optional[TreeNode], leaf_values: List[int]) -> None:
22 """
23 Perform DFS traversal to collect all leaf values in left-to-right order.
24
25 Args:
26 node: Current node in the traversal
27 leaf_values: List to store leaf values
28 """
29 # Base case: empty node
30 if not node:
31 return
32
33 # Check if current node is a leaf (no left and right children)
34 if node.left is None and node.right is None:
35 leaf_values.append(node.val)
36 return
37
38 # Recursively traverse left subtree
39 if node.left:
40 collect_leaves(node.left, leaf_values)
41
42 # Recursively traverse right subtree
43 if node.right:
44 collect_leaves(node.right, leaf_values)
45
46 # Collect leaf values from both trees
47 leaves_tree1 = []
48 leaves_tree2 = []
49
50 collect_leaves(root1, leaves_tree1)
51 collect_leaves(root2, leaves_tree2)
52
53 # Compare the leaf value sequences
54 return leaves_tree1 == leaves_tree2
55
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 * Determines if two binary trees have the same leaf value sequence.
19 * A leaf is a node with no children. The leaf value sequence is the
20 * sequence of values of all leaves from left to right.
21 *
22 * @param root1 The root of the first binary tree
23 * @param root2 The root of the second binary tree
24 * @return true if both trees have the same leaf value sequence, false otherwise
25 */
26 public boolean leafSimilar(TreeNode root1, TreeNode root2) {
27 // Collect leaf values from the first tree
28 List<Integer> leafValues1 = new ArrayList<>();
29 // Collect leaf values from the second tree
30 List<Integer> leafValues2 = new ArrayList<>();
31
32 // Perform depth-first search to collect leaves from both trees
33 collectLeaves(root1, leafValues1);
34 collectLeaves(root2, leafValues2);
35
36 // Compare the two leaf value sequences
37 return leafValues1.equals(leafValues2);
38 }
39
40 /**
41 * Performs a depth-first search to collect all leaf node values in order.
42 *
43 * @param node The current node being processed
44 * @param leafValues The list to store leaf values
45 */
46 private void collectLeaves(TreeNode node, List<Integer> leafValues) {
47 // Check if current node is a leaf node
48 // Note: node.left == node.right only when both are null
49 if (node.left == null && node.right == null) {
50 leafValues.add(node.val);
51 return;
52 }
53
54 // Recursively traverse the left subtree
55 if (node.left != null) {
56 collectLeaves(node.left, leafValues);
57 }
58
59 // Recursively traverse the right subtree
60 if (node.right != null) {
61 collectLeaves(node.right, leafValues);
62 }
63 }
64}
65
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 /**
15 * Determines if two binary trees have the same leaf value sequence
16 * @param root1 - Root of the first binary tree
17 * @param root2 - Root of the second binary tree
18 * @return true if both trees have identical leaf sequences, false otherwise
19 */
20 bool leafSimilar(TreeNode* root1, TreeNode* root2) {
21 // Collect leaf values from both trees
22 vector<int> leafSequence1, leafSequence2;
23
24 // Perform depth-first search to collect leaves
25 collectLeaves(root1, leafSequence1);
26 collectLeaves(root2, leafSequence2);
27
28 // Compare the two leaf sequences
29 return leafSequence1 == leafSequence2;
30 }
31
32private:
33 /**
34 * Helper function to collect all leaf node values using DFS
35 * @param node - Current node being processed
36 * @param leafValues - Vector to store leaf values in left-to-right order
37 */
38 void collectLeaves(TreeNode* node, vector<int>& leafValues) {
39 // Base case: null node
40 if (!node) {
41 return;
42 }
43
44 // Check if current node is a leaf (no left and right children)
45 if (!node->left && !node->right) {
46 leafValues.push_back(node->val);
47 return;
48 }
49
50 // Recursively traverse left subtree
51 if (node->left) {
52 collectLeaves(node->left, leafValues);
53 }
54
55 // Recursively traverse right subtree
56 if (node->right) {
57 collectLeaves(node->right, leafValues);
58 }
59 }
60};
61
1/**
2 * Definition for a binary tree node
3 */
4interface TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8}
9
10/**
11 * Determines if two binary trees have the same leaf value sequence
12 * @param root1 - Root node of the first binary tree
13 * @param root2 - Root node of the second binary tree
14 * @returns True if both trees have the same leaf value sequence, false otherwise
15 */
16const leafSimilar = function (root1: TreeNode, root2: TreeNode): boolean {
17 // Arrays to store leaf values for each tree
18 const leafValues1: number[] = [];
19 const leafValues2: number[] = [];
20
21 /**
22 * Performs depth-first search to collect leaf node values
23 * @param node - Current node being processed
24 * @param leafValues - Array to store leaf values
25 */
26 const dfs = (node: TreeNode | null, leafValues: number[]): void => {
27 // Base case: node is null
28 if (!node) {
29 return;
30 }
31
32 // Check if current node is a leaf (no left and right children)
33 if (node.left === null && node.right === null) {
34 leafValues.push(node.val);
35 return;
36 }
37
38 // Recursively traverse left subtree
39 if (node.left) {
40 dfs(node.left, leafValues);
41 }
42
43 // Recursively traverse right subtree
44 if (node.right) {
45 dfs(node.right, leafValues);
46 }
47 };
48
49 // Collect leaf values from both trees
50 dfs(root1, leafValues1);
51 dfs(root2, leafValues2);
52
53 // Compare leaf value sequences by converting to comma-separated strings
54 return leafValues1.join(',') === leafValues2.join(',');
55};
56
Time and Space Complexity
Time Complexity: O(n₁ + n₂)
where n₁
is the number of nodes in root1
and n₂
is the number of nodes in root2
. Since in the worst case both trees could have the same number of nodes n
, this simplifies to O(n)
. The algorithm performs a depth-first search (DFS) on both trees, visiting each node exactly once.
Space Complexity: O(h₁ + h₂ + l₁ + l₂)
where h₁
and h₂
are the heights of the two trees (for the recursion call stack), and l₁
and l₂
are the number of leaf nodes in each tree (for storing the leaf values in lists). In the worst case, the height of a tree can be O(n)
for a skewed tree, and the number of leaves can be up to O(n/2)
for a complete binary tree. Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Leaf Order Due to Wrong Traversal
A common mistake is collecting leaves in the wrong order by not maintaining a consistent traversal pattern. Some developers might accidentally mix pre-order, in-order, or post-order traversal logic, or traverse right before left.
Incorrect Implementation:
def collect_leaves(node, leaf_values):
if not node:
return
if not node.left and not node.right:
leaf_values.append(node.val)
# Wrong: traversing right before left
collect_leaves(node.right, leaf_values)
collect_leaves(node.left, leaf_values)
Solution: Always traverse left subtree before right subtree to maintain left-to-right order:
def collect_leaves(node, leaf_values):
if not node:
return
if not node.left and not node.right:
leaf_values.append(node.val)
return
collect_leaves(node.left, leaf_values) # Left first
collect_leaves(node.right, leaf_values) # Then right
2. Creating New Lists in Recursive Calls
A critical error is creating new lists in each recursive call instead of passing the same list by reference, resulting in lost leaf values.
Incorrect Implementation:
def collect_leaves(node):
leaf_values = [] # Creates new list each time!
if not node:
return leaf_values
if not node.left and not node.right:
leaf_values.append(node.val)
return leaf_values
# These returned values are never captured
collect_leaves(node.left)
collect_leaves(node.right)
return leaf_values
Solution: Pass the list as a parameter to maintain a single accumulator:
def collect_leaves(node, leaf_values): # Pass list as parameter
if not node:
return
if not node.left and not node.right:
leaf_values.append(node.val)
return
collect_leaves(node.left, leaf_values)
collect_leaves(node.right, leaf_values)
3. Incorrectly Identifying Leaf Nodes
Some implementations fail to properly identify leaf nodes, potentially treating nodes with one child as leaves or missing actual leaves.
Incorrect Implementation:
# Wrong: This treats nodes with one null child as leaves if not node.left or not node.right: leaf_values.append(node.val) # Wrong: Overly complex and error-prone if node.left is None: if node.right is None: leaf_values.append(node.val)
Solution: Use clear and correct leaf identification:
# Correct and clear if node.left is None and node.right is None: leaf_values.append(node.val) # Or the elegant version from the original if node.left == node.right: # Both are None leaf_values.append(node.val)
4. Not Handling Edge Cases
Failing to handle null trees or single-node trees can cause runtime errors.
Incorrect Implementation:
def leafSimilar(self, root1, root2):
leaves1, leaves2 = [], []
# Doesn't check if roots are None
self.collect_leaves(root1, leaves1)
self.collect_leaves(root2, leaves2)
return leaves1 == leaves2
Solution: The helper function should handle None gracefully:
def collect_leaves(node, leaf_values):
if not node: # Handle None nodes
return
# Rest of the logic...
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
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
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!