Leaf-Similar Trees
EasyPrompt
Consider all the leaves of a binary tree, from left to right order, the values of those leaves form a leaf value sequence.

For example, in the given tree above, the leaf value sequence is (6, 7, 4, 9, 8).
Two binary trees are considered leaf-similar if their leaf value sequence is the same.
Return true if and only if the two given trees with head nodes root1 and root2 are leaf-similar.
Example 1:
Input: root1 = [3,5,1,6,2,9,8,null,null,7,4], root2 = [3,5,1,6,7,4,2,null,null,null,null,null,null,9,8]
Output: trueExample 2:
Input: root1 = [1,2,3], root2 = [1,3,2]
Output: false
Constraints:
- The number of nodes in each tree will be in the range
[1, 200]. - Both of the given trees will have values in the range
[0, 200].
Approaches
2 approaches with complexity analysis and trade-offs.
This approach involves performing a Depth-First Search (DFS) on each tree to collect all its leaf values into a list. After traversing both trees and populating two separate lists with their respective leaf value sequences, the two lists are compared to check for equality.
Algorithm
- Initialize two empty lists,
leaves1andleaves2. - Define a recursive DFS helper function
getLeaves(node, list). - In
getLeaves, ifnodeis null, return. - If
nodeis a leaf (node.left == null && node.right == null), addnode.valto thelist. - Recursively call
getLeavesfor the left child:getLeaves(node.left, list). - Recursively call
getLeavesfor the right child:getLeaves(node.right, list). - Call
getLeaves(root1, leaves1)to populate the first list. - Call
getLeaves(root2, leaves2)to populate the second list. - Return the result of comparing
leaves1andleaves2for equality.
Walkthrough
We define a helper function, say getLeaves, that takes a tree node and a list as input. This function will traverse the tree starting from the given node.
The traversal can be pre-order, in-order, or post-order; the key is to visit children in a consistent order (left then right) to maintain the left-to-right sequence of leaves.
Inside the getLeaves function, we check if the current node is a leaf (i.e., it has no left and no right child). If it is, we add its value to the list.
The main function leafSimilar initializes two empty lists. It then calls getLeaves for root1 and the first list, and then for root2 and the second list.
Finally, it compares the two populated lists. If they are identical in content and order, the trees are leaf-similar, and we return true; otherwise, false.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public boolean leafSimilar(TreeNode root1, TreeNode root2) { List<Integer> leaves1 = new ArrayList<>(); List<Integer> leaves2 = new ArrayList<>(); getLeaves(root1, leaves1); getLeaves(root2, leaves2); return leaves1.equals(leaves2); } private void getLeaves(TreeNode node, List<Integer> leafValues) { if (node == null) { return; } if (node.left == null && node.right == null) { leafValues.add(node.val); } getLeaves(node.left, leafValues); getLeaves(node.right, leafValues); }}Complexity
Time
O(N1 + N2), where N1 and N2 are the number of nodes in the first and second trees, respectively. We must visit every node in both trees to find all the leaves. Comparing the two lists of leaves also takes time proportional to their lengths, which is at most O(N1 + N2).
Space
O(L1 + L2 + H1 + H2), where L1 and L2 are the number of leaves, and H1 and H2 are the heights of the trees. The space is used to store the leaf value sequences in lists (O(L1 + L2)) and for the recursion call stack (O(H1 + H2)). In the worst case of skewed trees, this becomes O(N1 + N2).
Trade-offs
Pros
Simple and intuitive to implement.
The logic is straightforward, separating the leaf-finding from the comparison.
Cons
Uses significant extra space to store the entire leaf sequences, which can be large.
It traverses both trees completely, even if a mismatch is found early in the leaf sequence.
This approach avoids storing the entire leaf sequences by comparing leaves one by one as they are found. It uses an iterative Depth-First Search (DFS) with a stack to simulate a generator that yields the next leaf of a tree. By running two such 'generators' in parallel, one for each tree, we can fetch and compare leaves on the fly.
Algorithm
- Initialize two stacks,
s1ands2, and pushroot1androot2onto them, respectively. - Create a helper function
findNextLeaf(stack)that finds and returns the value of the next leaf. - Inside
findNextLeaf, loop while the stack is not empty:- Pop a
nodefrom the stack. - If the
nodehas a right child, push it onto the stack. - If the
nodehas a left child, push it onto the stack. (This order ensures left-to-right traversal). - If the
nodeis a leaf, return its value.
- Pop a
- In the main function, loop while both
s1ands2are not empty. - Inside the loop, call
findNextLeaf(s1)andfindNextLeaf(s2)and compare their results. If they are not equal, returnfalse. - After the loop, check if both stacks are empty. If they are, it means both leaf sequences were identical and have been fully traversed. Return
true. Otherwise, one sequence was longer than the other, so returnfalse.
Walkthrough
Instead of a recursive DFS, we use a stack for an iterative pre-order traversal. This allows us to 'pause' the traversal after finding a leaf and 'resume' it later to find the next one.
We create a helper function, findNextLeaf, which takes a stack (representing the state of the traversal for one tree) and finds the next leaf value.
The findNextLeaf function works in a loop: it pops a node from the stack. If the node is a leaf, its value is returned. If it's not a leaf, its children are pushed onto the stack (right child first, then left child) to ensure the left-to-right pre-order traversal.
The main function leafSimilar initializes two stacks, one for each tree, pushing the respective roots.
It then enters a loop, calling findNextLeaf for both stacks in each iteration to get the next pair of leaves.
If the leaf values differ, it immediately returns false. If one sequence ends before the other, it also returns false. If both sequences end at the same time (both findNextLeaf calls indicate no more leaves), it returns true.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public boolean leafSimilar(TreeNode root1, TreeNode root2) { Stack<TreeNode> s1 = new Stack<>(); Stack<TreeNode> s2 = new Stack<>(); if (root1 != null) s1.push(root1); if (root2 != null) s2.push(root2); while (!s1.isEmpty() && !s2.isEmpty()) { if (findNextLeaf(s1) != findNextLeaf(s2)) { return false; } } return s1.isEmpty() && s2.isEmpty(); } private int findNextLeaf(Stack<TreeNode> stack) { while (true) { TreeNode node = stack.pop(); if (node.right != null) { stack.push(node.right); } if (node.left != null) { stack.push(node.left); } if (node.left == null && node.right == null) { return node.val; } } }}Complexity
Time
O(N1 + N2). Although it can terminate early, in the worst case (when the trees are leaf-similar or the mismatch occurs late), we still need to traverse all nodes of both trees. Each node is pushed and popped from its stack once.
Space
O(H1 + H2), where H1 and H2 are the maximum heights of the two trees. The space is dominated by the stacks used for the iterative DFS. This is a significant improvement over the first approach, especially for balanced trees where H is O(log N).
Trade-offs
Pros
Highly space-efficient, using space proportional to the tree height rather than the number of nodes or leaves.
More time-efficient in practice for non-similar trees, as it can terminate as soon as the first mismatch is found.
Cons
The implementation is more complex than the simple recursive approach.
Managing the state of the traversal with an explicit stack can be less intuitive.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public boolean leafSimilar ( TreeNode root1 , TreeNode root2 ) { List < Integer > l1 = dfs ( root1 ); List < Integer > l2 = dfs ( root2 ); return l1 . equals ( l2 ); } private List < Integer > dfs ( TreeNode root ) { if ( root == null ) { return new ArrayList <>(); } List < Integer > ans = dfs ( root . left ); ans . addAll ( dfs ( root . right )); if ( ans . isEmpty ()) { ans . add ( root . val ); } return ans ; } }var leafSimilar = function ( root1 , root2 ) { const dfs = root => { if ( ! root ) { return []; } let ans = [... dfs ( root . left ), ... dfs ( root . right )]; if ( ! ans . length ) { ans = [ root . val ]; } return ans ; }; const l1 = dfs ( root1 ); const l2 = dfs ( root2 ); return l1 . toString () === l2 . toString (); };/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: bool leafSimilar ( TreeNode * root1 , TreeNode * root2 ) { return dfs ( root1 ) == dfs ( root2 ); } vector < int > dfs ( TreeNode * root ) { if ( ! root ) return {}; auto ans = dfs ( root -> left ); auto right = dfs ( root -> right ); ans . insert ( ans . end (), right . begin (), right . end ()); if ( ans . empty ()) ans . push_back ( root -> val ); return ans ; } };# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution : def leafSimilar ( self , root1 : Optional [ TreeNode ], root2 : Optional [ TreeNode ]) -> bool : def dfs ( root ): if root is None : return [] ans = dfs ( root . left ) + dfs ( root . right ) return ans or [ root . val ] return dfs ( root1 ) == dfs ( root2 )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.