572. Subtree of Another Tree
Problem Description
You are given two binary trees with roots called root
and subRoot
. Your task is to determine if subRoot
appears as a subtree anywhere within root
.
A subtree is defined as a tree that consists of a node and all of its descendants. For a tree to be considered a subtree of another, it must have:
- The exact same structure
- The exact same node values at corresponding positions
The main tree root
is also considered a subtree of itself.
Return true
if subRoot
exists as a subtree within root
, and false
otherwise.
For example, if you have a tree root
with nodes arranged in a certain pattern, and another smaller tree subRoot
, you need to check if you can find a node in root
where starting from that node, the structure and values exactly match subRoot
.
The solution uses a depth-first search approach with two key components:
- A helper function
same(p, q)
that checks if two trees are identical by comparing their root values and recursively checking their left and right subtrees - The main function that traverses through each node in
root
and checks if the subtree starting from that node matchessubRoot
The algorithm works by:
- First checking if the current node's subtree matches
subRoot
completely - If not, recursively checking if
subRoot
exists in either the left or right subtree of the current node - Returning
true
if any match is found,false
if no match exists after checking all possibilities
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: While not explicitly a graph problem, a binary tree is a special type of graph (specifically, a connected acyclic graph). Binary trees have nodes connected by edges (parent-child relationships).
Is it a tree?
- Yes: The problem explicitly deals with binary trees. We have two binary tree structures (
root
andsubRoot
) and need to check if one appears as a subtree within the other.
DFS
- Yes: Since we're working with trees and need to explore each node to check if it could be the starting point of a matching subtree, DFS is the appropriate approach.
Conclusion: The flowchart leads us directly to using DFS (Depth-First Search) for this problem. This makes sense because:
- We need to traverse through each node in the main tree
- At each node, we need to check if the subtree rooted at that node matches
subRoot
- The checking itself requires comparing entire tree structures recursively
- DFS allows us to systematically explore each path in the tree and perform the necessary comparisons
The DFS pattern manifests in two ways in the solution:
- The main traversal through
root
to find potential starting points - The recursive comparison of tree structures using the
same()
helper function
Intuition
When we need to check if one tree is a subtree of another, the natural approach is to think about what makes two trees identical. If we can identify when two trees are the same, then we can check every possible starting position in the main tree to see if any of them match our target subtree.
Think of it like searching for a specific pattern in a larger structure. We need to:
- Know how to verify if two patterns match exactly
- Check every possible position where the pattern could start
For the first part, two trees are identical when:
- Their root values are the same
- Their left subtrees are identical
- Their right subtrees are identical
This naturally leads to a recursive comparison function that checks these three conditions.
For the second part, since subRoot
could appear anywhere within root
, we need to check:
- Could
subRoot
match starting from the current root node? - If not, could it be found in the left subtree?
- If not there either, could it be in the right subtree?
This is where DFS comes in naturally - we're systematically exploring each node as a potential starting point for our match. At each node we visit, we ask "Does the subtree starting here match subRoot
?" If yes, we're done. If no, we continue searching in the children.
The beauty of this approach is that it breaks down a complex problem into two simpler recursive problems:
- Checking if two trees are identical (the
same
function) - Searching through all nodes to find where to apply that check (the main
isSubtree
function)
Both naturally fit the DFS pattern because trees have a recursive structure - each subtree is itself a tree with the same properties as the whole.
Learn more about Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The solution implements a DFS approach with two key functions working together:
Helper Function: same(p, q)
This function determines if two trees are identical:
- Base case: If either
p
orq
isNone
, they're only identical if both areNone
(checked withp is q
) - Recursive case: Trees are identical if:
- Root values match:
p.val == q.val
- Left subtrees are identical:
same(p.left, q.left)
- Right subtrees are identical:
same(p.right, q.right)
- Root values match:
All three conditions must be true, so we use the and
operator to combine them.
Main Function: isSubtree(root, subRoot)
This function traverses the main tree looking for a matching subtree:
-
Base case: If
root
isNone
, returnFalse
(an empty tree cannot contain a non-empty subtree) -
Check current position: Call
same(root, subRoot)
to check if the tree rooted at the current node matchessubRoot
-
Recursive search: If the current position doesn't match, recursively search:
- Left subtree:
self.isSubtree(root.left, subRoot)
- Right subtree:
self.isSubtree(root.right, subRoot)
- Left subtree:
-
Return result: Use the
or
operator to returnTrue
if any of the three checks succeed
Algorithm Flow
The algorithm works by:
- Starting at the root of the main tree
- At each node, checking if the subtree rooted there matches
subRoot
completely - If not, recursively checking both left and right children
- Propagating
True
up the call stack as soon as any match is found
Time and Space Complexity
-
Time Complexity:
O(m × n)
wherem
is the number of nodes inroot
andn
is the number of nodes insubRoot
. In the worst case, we check every node inroot
(m nodes) and for each check, we might compare all nodes insubRoot
(n nodes). -
Space Complexity:
O(h)
whereh
is the height ofroot
, due to the recursive call stack. In the worst case (skewed tree), this could beO(m)
.
The elegance of this solution lies in its simplicity - by separating the "matching" logic from the "searching" logic, we create two clean, recursive functions that each handle one specific responsibility.
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 see how the solution works.
Consider these two trees:
root: subRoot: 3 4 / \ / \ 4 5 1 2 / \ 1 2
We want to check if subRoot
appears as a subtree within root
.
Step 1: Start at root node (3)
- Call
same(node_3, subRoot_root_4)
- Compare values: 3 ≠ 4, so these trees aren't identical
- Since they don't match, continue searching
Step 2: Check left subtree of root
- Call
isSubtree(node_4, subRoot)
- Now at node 4, call
same(node_4, subRoot_root_4)
- Compare values: 4 = 4 ✓
- Recursively check left children:
same(node_1, subRoot_node_1)
- Values match: 1 = 1 ✓
- Both have no children (null = null) ✓
- Recursively check right children:
same(node_2, subRoot_node_2)
- Values match: 2 = 2 ✓
- Both have no children (null = null) ✓
- All checks pass! Return
true
The algorithm finds that the left subtree of root
(starting at node 4) exactly matches subRoot
, so it returns true
.
Let's trace through what would happen if subRoot
was NOT in the tree:
root: subRoot: 3 7 / \ / \ 4 5 1 2 / \ 1 2
Step 1: Check root (3) - doesn't match subRoot root (7) Step 2: Check left subtree (4) - doesn't match subRoot root (7) Step 3: Recursively check node 4's children:
- Node 1 doesn't match (1 ≠ 7, and node 1 has no children while subRoot has children)
- Node 2 doesn't match (2 ≠ 7, and node 2 has no children while subRoot has children) Step 4: Check right subtree (5) - doesn't match subRoot root (7) Step 5: Node 5 has no children to check
Since no position matched, return false
.
The key insight is that at each node, we perform a complete tree comparison using same()
. If that fails, we keep searching deeper in the tree using DFS until we either find a match or exhaust all possibilities.
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 isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
10 """
11 Determines if subRoot is a subtree of root.
12 A subtree of a binary tree is a tree that consists of a node in the tree
13 and all of its descendants.
14 """
15
16 def is_same_tree(tree1: Optional[TreeNode], tree2: Optional[TreeNode]) -> bool:
17 """
18 Helper function to check if two trees are identical.
19 Two trees are identical if they have the same structure and node values.
20 """
21 # Base case: if either tree is None, both must be None to be identical
22 if tree1 is None or tree2 is None:
23 return tree1 is tree2
24
25 # Check if current nodes match and recursively check left and right subtrees
26 return (tree1.val == tree2.val and
27 is_same_tree(tree1.left, tree2.left) and
28 is_same_tree(tree1.right, tree2.right))
29
30 # Base case: if root is None, subRoot cannot be its subtree
31 if root is None:
32 return False
33
34 # Check if current tree matches subRoot, or if subRoot exists in left or right subtree
35 return (is_same_tree(root, subRoot) or
36 self.isSubtree(root.left, subRoot) or
37 self.isSubtree(root.right, subRoot))
38
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 subRoot is a subtree of root.
19 * A subtree of a binary tree is a tree that consists of a node and all of its descendants.
20 *
21 * @param root The root of the main binary tree
22 * @param subRoot The root of the potential subtree
23 * @return true if subRoot is a subtree of root, false otherwise
24 */
25 public boolean isSubtree(TreeNode root, TreeNode subRoot) {
26 // Base case: if main tree is empty, it cannot contain a subtree
27 if (root == null) {
28 return false;
29 }
30
31 // Check if current node matches the subtree OR
32 // recursively check if subtree exists in left or right branches
33 return isSameTree(root, subRoot)
34 || isSubtree(root.left, subRoot)
35 || isSubtree(root.right, subRoot);
36 }
37
38 /**
39 * Helper method to check if two trees are identical.
40 * Two trees are identical if they have the same structure and node values.
41 *
42 * @param treeOne The root of the first tree
43 * @param treeTwo The root of the second tree
44 * @return true if both trees are identical, false otherwise
45 */
46 private boolean isSameTree(TreeNode treeOne, TreeNode treeTwo) {
47 // If either node is null, both must be null for trees to be identical
48 if (treeOne == null || treeTwo == null) {
49 return treeOne == treeTwo;
50 }
51
52 // Check if current nodes have same value AND
53 // recursively verify that left and right subtrees are also identical
54 return treeOne.val == treeTwo.val
55 && isSameTree(treeOne.left, treeTwo.left)
56 && isSameTree(treeOne.right, treeTwo.right);
57 }
58}
59
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 subRoot is a subtree of root
16 * A subtree of a tree T is a tree consisting of a node in T and all of its descendants
17 * @param root The main tree to search in
18 * @param subRoot The tree to search for as a subtree
19 * @return true if subRoot is a subtree of root, false otherwise
20 */
21 bool isSubtree(TreeNode* root, TreeNode* subRoot) {
22 // Base case: if root is null, subRoot cannot be its subtree
23 if (!root) {
24 return false;
25 }
26
27 // Check if current tree rooted at 'root' matches subRoot
28 // OR recursively check if subRoot is a subtree of left child
29 // OR recursively check if subRoot is a subtree of right child
30 return isSameTree(root, subRoot) ||
31 isSubtree(root->left, subRoot) ||
32 isSubtree(root->right, subRoot);
33 }
34
35private:
36 /**
37 * Helper function to check if two trees are identical
38 * @param tree1 First tree to compare
39 * @param tree2 Second tree to compare
40 * @return true if both trees are structurally identical with same values, false otherwise
41 */
42 bool isSameTree(TreeNode* tree1, TreeNode* tree2) {
43 // Both nodes are null - trees are identical at this point
44 if (!tree1 || !tree2) {
45 return tree1 == tree2;
46 }
47
48 // Both nodes exist - check if values match and recursively check subtrees
49 return tree1->val == tree2->val &&
50 isSameTree(tree1->left, tree2->left) &&
51 isSameTree(tree1->right, tree2->right);
52 }
53};
54
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 * Checks if two trees are identical in structure and node values
17 * @param treeOne - First tree to compare
18 * @param treeTwo - Second tree to compare
19 * @returns true if both trees are identical, false otherwise
20 */
21function isSameTree(treeOne: TreeNode | null, treeTwo: TreeNode | null): boolean {
22 // Base case: if either tree is null, both must be null to be identical
23 if (!treeOne || !treeTwo) {
24 return treeOne === treeTwo;
25 }
26
27 // Check if current nodes have same value and recursively check left and right subtrees
28 return treeOne.val === treeTwo.val &&
29 isSameTree(treeOne.left, treeTwo.left) &&
30 isSameTree(treeOne.right, treeTwo.right);
31}
32
33/**
34 * Determines if subRoot is a subtree of root
35 * A subtree of a tree consists of a node and all of its descendants
36 * @param root - The main tree to search within
37 * @param subRoot - The tree to find as a subtree
38 * @returns true if subRoot is a subtree of root, false otherwise
39 */
40function isSubtree(root: TreeNode | null, subRoot: TreeNode | null): boolean {
41 // Base case: if main tree is null, it cannot contain any subtree
42 if (!root) {
43 return false;
44 }
45
46 // Check if current tree matches subRoot, or if subRoot exists in left or right subtree
47 return isSameTree(root, subRoot) ||
48 isSubtree(root.left, subRoot) ||
49 isSubtree(root.right, subRoot);
50}
51
Time and Space Complexity
Time Complexity: O(n × m)
The algorithm checks if subRoot
is a subtree of root
by:
- For each node in the main tree
root
(which hasn
nodes), we potentially call thesame
function - The
same
function compares two trees for equality, which in the worst case needs to traverse allm
nodes ofsubRoot
- In the worst case, we check every node in
root
as a potential match, and for each check, we traverse up tom
nodes insubRoot
- Therefore, the time complexity is
O(n × m)
Space Complexity: O(n)
The space complexity comes from the recursion stack:
- The
isSubtree
function recursively traverses the main treeroot
, which can go as deep as the height of the tree - In the worst case (a skewed tree), the height equals
n
, so the recursion stack forisSubtree
usesO(n)
space - The
same
function also uses recursion, but its stack depth is bounded bymin(height of root subtree, height of subRoot)
, which is at mostO(n)
- Since these recursive calls don't happen simultaneously (we finish one
same
call before moving to the next node inisSubtree
), the maximum stack depth isO(n)
- Therefore, the space complexity is
O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing Subtree with Substructure
A critical pitfall is misunderstanding what constitutes a valid subtree. A subtree must include all descendants of a node, not just a partial match.
Incorrect Understanding:
root: subRoot:
1 1
/ \ \
1 3 3
\
3
Many think subRoot matches the left portion of root, but it doesn't!
The left "1" in root has a child "3", while subRoot's "1" has no left child.
Correct Understanding: A subtree match requires the entire structure from a node downward to be identical, including where nodes are absent (None values matter!).
2. Incorrect Base Case Handling
Pitfall Code:
def is_same_tree(tree1, tree2):
if not tree1 and not tree2:
return True
if not tree1 or not tree2: # Redundant after first check
return False
# ... rest of logic
Better Approach:
def is_same_tree(tree1, tree2):
if tree1 is None or tree2 is None:
return tree1 is tree2 # Elegant one-liner handling all None cases
3. Missing Edge Case: Empty SubRoot
The problem statement typically assumes subRoot
is not None, but if it could be:
Problematic Scenario:
# If subRoot is None, should this return True or False? isSubtree(root, None)
Solution: Add explicit handling:
def isSubtree(self, root, subRoot):
if subRoot is None:
return True # Empty tree is subtree of any tree
if root is None:
return False # Non-empty subRoot can't be in empty root
# ... rest of logic
4. Performance Degradation with Repeated Values
When the tree has many nodes with the same value as subRoot
's root, the algorithm repeatedly performs full tree comparisons:
Worst Case Example:
root: subRoot: 1 1 / \ / 1 1 2 / \ / \ 1 1 1 1
Every node with value "1" triggers a full comparison with subRoot
.
Optimization Strategy: Consider using tree serialization or hashing for large trees with many repeated values:
def isSubtree(self, root, subRoot):
def serialize(node):
if not node:
return "#"
return f",{node.val},{serialize(node.left)},{serialize(node.right)}"
tree_str = serialize(root)
subtree_str = serialize(subRoot)
return subtree_str in tree_str
5. Stack Overflow with Deep Trees
For extremely deep trees, the recursive approach can cause stack overflow.
Solution for Production Code: Implement an iterative version using explicit stack:
def isSubtree(self, root, subRoot):
def is_same_tree_iterative(p, q):
stack = [(p, q)]
while stack:
node1, node2 = stack.pop()
if not node1 and not node2:
continue
if not node1 or not node2 or node1.val != node2.val:
return False
stack.append((node1.left, node2.left))
stack.append((node1.right, node2.right))
return True
if not root:
return False
stack = [root]
while stack:
node = stack.pop()
if is_same_tree_iterative(node, subRoot):
return True
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return False
Key Takeaway
The most common mistake is assuming partial structural matches count as subtrees. Remember: a subtree must be an exact copy of a node and all its descendants - no additions, no omissions, no reorderings.
Which two pointer techniques do you use to check if a string is a palindrome?
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!