Facebook Pixel

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:

  1. Find the leaf value sequence of the first tree (root1)
  2. Find the leaf value sequence of the second tree (root2)
  3. 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 are None), 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:

  1. We need to explore each path from root to leaf completely before moving to the next path
  2. DFS naturally visits leaves in left-to-right order when we process left subtrees before right subtrees
  3. The recursive nature of DFS matches the recursive structure of trees perfectly
  4. We can easily identify and collect leaf nodes during the traversal when we encounter nodes with no children
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Visits every node to find all leaves
  2. 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:

  1. Extract leaf sequences from each tree
  2. 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:

  1. Helper Function Design: We create a dfs helper function that takes two parameters:

    • root: The current node being visited
    • nums: A list that accumulates leaf values during traversal
  2. Leaf Node Detection: The key insight is the condition root.left == root.right. This checks if both children are None (since None == None is True), elegantly identifying leaf nodes without explicitly checking root.left is None and root.right is None.

  3. 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.

  4. List as Accumulator: We use Python lists l1 and l2 to accumulate leaf values. The lists are passed by reference to the recursive function, so all recursive calls append to the same list.

  5. Main Logic Flow:

    • Initialize two empty lists: l1 = [] and l2 = []
    • Call dfs(root1, l1) to populate l1 with leaves from the first tree
    • Call dfs(root2, l2) to populate l2 with leaves from the second tree
    • Return l1 == l2 to check if the sequences are identical

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 Evaluator

Example 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 = []:

  1. At node 3: Not a leaf (has children), so traverse left to node 5
  2. At node 5: Not a leaf (has children), so traverse left to node 6
  3. At node 6: This is a leaf! (left == right == None), add 6 to l1: l1 = [6]
  4. Back to node 5, traverse right to node 2
  5. At node 2: This is a leaf! Add 2 to l1: l1 = [6, 2]
  6. Back to node 3, traverse right to node 1
  7. 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 = []:

  1. At node 5: Not a leaf (has children), so traverse left to node 6
  2. At node 6: This is a leaf! Add 6 to l2: l2 = [6]
  3. Back to node 5, traverse right to node 1
  4. At node 1: Not a leaf (has right child), skip left, traverse right to node 2
  5. At node 2: This is a leaf! Add 2 to l2: l2 = [6, 2]
  6. Back to node 1: No more children to visit
  7. 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] returns False

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...
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More