Subtree of Another Tree
EasyPrompt
Given the roots of two binary trees root and subRoot, return true if there is a subtree of root with the same structure and node values of subRoot and false otherwise.
A subtree of a binary tree tree is a tree that consists of a node in tree and all of this node's descendants. The tree tree could also be considered as a subtree of itself.
Example 1:
Input: root = [3,4,5,1,2], subRoot = [4,1,2]
Output: trueExample 2:
Input: root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]
Output: false
Constraints:
- The number of nodes in the
roottree is in the range[1, 2000]. - The number of nodes in the
subRoottree is in the range[1, 1000]. -104 <= root.val <= 104-104 <= subRoot.val <= 104
Approaches
3 approaches with complexity analysis and trade-offs.
This approach involves traversing the main tree (root). For each node encountered in root, we check if the subtree starting at that node is identical to the subRoot tree. This is the most straightforward, brute-force solution.
Algorithm
- Define a main recursive function
isSubtree(root, subRoot). - Define a helper recursive function
isSameTree(p, q)to check if two trees are identical. - In
isSubtree:- Handle the base case: if
rootisnull, returnfalse. - Check if the tree starting at the current
rootis identical tosubRootby callingisSameTree(root, subRoot). If it returnstrue, a match is found, so returntrue. - If no match is found at the current node, the subtree might exist deeper in the tree. Recursively search in the left and right children:
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot).
- Handle the base case: if
- In
isSameTree:- If both nodes
pandqarenull, they are identical, returntrue. - If one node is
null, or their values differ, they are not identical, returnfalse. - Recursively compare the left children and the right children:
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right).
- If both nodes
Walkthrough
This method uses two recursive functions. The main function, isSubtree, traverses every node of the root tree. For each of these nodes, it calls a helper function, isSameTree, to determine if the subtree rooted at the current node is structurally and valuably identical to the subRoot tree.
If isSameTree finds a match, the process stops and returns true. If it doesn't, isSubtree moves to the children of the current node and repeats the process. If the entire root tree is traversed without finding an identical subtree, the function returns 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 isSubtree(TreeNode root, TreeNode subRoot) { if (root == null) { return false; } if (isSameTree(root, subRoot)) { return true; } return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot); } private boolean isSameTree(TreeNode p, TreeNode q) { if (p == null && q == null) { return true; } if (p == null || q == null || p.val != q.val) { return false; } return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); }}Complexity
Time
O(m * n), where `m` is the number of nodes in `root` and `n` is the number of nodes in `subRoot`. In the worst-case scenario, we might call `isSameTree` for every node in `root`, and each call to `isSameTree` takes O(n) time.
Space
O(m + n) in the worst case. The space is determined by the maximum depth of the recursion stack. In a skewed tree, the depth of `isSubtree` can be `m` and the depth of `isSameTree` can be `n`. Since the calls are nested, the total depth can be `m + n`.
Trade-offs
Pros
Simple to understand and implement.
Requires minimal extra space, used only by the recursion stack.
Cons
Highly inefficient for large or skewed trees due to its quadratic time complexity.
Performs many redundant comparisons on the same nodes.
A more efficient approach is to convert both trees into strings, for example, using a pre-order traversal. Once both root and subRoot are represented as strings, the problem is reduced to a simple substring search. If the subRoot's string is a substring of the root's string, it means the subtree exists.
Algorithm
- Define a
serialize(node, stringBuilder)function that performs a pre-order traversal. - To uniquely identify the structure, append a special marker (e.g.,
"#") fornullnodes and a separator (e.g.,",") between values. - Generate the serialized string for
root(s1) andsubRoot(s2). It's important to start each serialization with a separator to handle edge cases (e.g., distinguishing a subtree with value2from a node with value12). - Use a standard library function to check if
s1containss2. - Return the result of the substring search.
Walkthrough
To ensure the serialization is unambiguous, we must represent the tree's structure, including null children. A pre-order traversal (Node -> Left -> Right) is a common choice. For each node, we append its value. For a null child, we append a special marker like '#'. We also use a separator like ',' to distinguish between node values (e.g., to tell 12 apart from 1 and 2).
After generating the string for root (s1) and subRoot (s2), we can use a built-in function like String.contains() in Java to perform the check. This check is typically implemented with an efficient algorithm like KMP, leading to a linear time complexity overall.
class Solution { public boolean isSubtree(TreeNode root, TreeNode subRoot) { // Using StringBuilder for efficient string construction. StringBuilder rootString = new StringBuilder(); serialize(root, rootString); StringBuilder subRootString = new StringBuilder(); serialize(subRoot, subRootString); // The .toString() conversion creates the final strings. // String.contains() checks for the substring. return rootString.toString().contains(subRootString.toString()); } private void serialize(TreeNode node, StringBuilder sb) { if (node == null) { // Use a special marker for null nodes to preserve structure. sb.append(",#"); return; } // Prepend a separator to distinguish nodes, e.g., 12 from 1,2. sb.append(",").append(node.val); serialize(node.left, sb); serialize(node.right, sb); }}Complexity
Time
O(m + n). Serializing `root` takes `O(m)` and `subRoot` takes `O(n)`. The substring search, if implemented efficiently (like KMP), also takes `O(m + n)` time. Thus, the total time complexity is linear.
Space
O(m + n). This space is needed to store the two serialized strings. The length of the string for `root` is proportional to `m`, and for `subRoot` is proportional to `n`. The recursion stack for serialization also contributes up to `O(m)` space in the worst case.
Trade-offs
Pros
Achieves linear time complexity, a significant improvement over the brute-force method.
Conceptually straightforward: reduces a tree problem to a string problem.
Cons
Requires significant extra space to store the serialized strings, which can be very large.
String concatenation and substring search can have high constant factor overhead compared to numeric operations.
The most efficient approach in practice is often Merkle Hashing. This technique avoids the overhead of creating and manipulating large strings by instead computing a numeric hash for each subtree. A hash is a number that uniquely represents the contents and structure of a subtree. If the hash of subRoot matches the hash of any subtree in root, we likely have a match. A final, direct comparison is done to confirm, just in case of a hash collision.
Algorithm
- Define a recursive function
computeHash(node)that performs a post-order traversal and returns a numeric hash for the subtree atnode. - The hash should be computed from the node's value and the hashes of its children:
hash = (node.val + left_hash * p1 + right_hash * p2) % MOD. Use prime multipliers (p1,p2) and a large modulus (MOD) to minimize collisions. - First, call
computeHash(subRoot)to calculate thetargetHash. - Next, traverse the
roottree. For eachnodeinroot, compute its subtree's hash. - If a computed hash matches
targetHash, this indicates a potential match. To be certain, call a definitiveisSameTree(node, subRoot)checker to handle the (rare) case of a hash collision. - If
isSameTreereturnstrue, a match is found. If the entireroottree is traversed without a confirmed match, returnfalse.
Walkthrough
This method works by performing two main traversals. First, we traverse subRoot to compute its hash value. This hash is calculated recursively in a post-order manner, where a parent node's hash is a function of its own value and the hashes of its left and right children. This ensures that both values and structure contribute to the final hash.
Second, we traverse the root tree. At each node, we compute the hash of the subtree rooted there using the same function. If this hash matches the target hash of subRoot, we perform an explicit isSameTree comparison to verify it's not a coincidental collision. Because numeric operations are very fast and good hash functions make collisions rare, this method is typically faster than string-based serialization.
import java.util.ArrayList;import java.util.List; class Solution { private long subRootHash; private TreeNode subRootNode; private boolean matchFound = false; // Primes for hashing to mix the values private final long P1 = 31; private final long P2 = 37; private final long MOD = 1_000_000_007; public boolean isSubtree(TreeNode root, TreeNode subRoot) { this.subRootNode = subRoot; // 1. Compute hash of the target subtree (subRoot) this.subRootHash = computeHash(subRoot); // 2. Traverse the main tree, compute hashes and check for match traverseAndCheck(root); return matchFound; } private void traverseAndCheck(TreeNode node) { if (node == null || matchFound) { return; } if (computeHash(node) == this.subRootHash) { if (isSameTree(node, this.subRootNode)) { matchFound = true; return; } } traverseAndCheck(node.left); traverseAndCheck(node.right); } private long computeHash(TreeNode node) { if (node == null) { return 0; // A consistent hash for null nodes } long leftHash = computeHash(node.left); long rightHash = computeHash(node.right); // Post-order processing: compute hash after children are processed // Offset val to handle negative values and 0, ensuring it's positive. long currentHash = (node.val + 10001 + (leftHash * P1) + (rightHash * P2)) % MOD; return currentHash; } private boolean isSameTree(TreeNode p, TreeNode q) { if (p == null && q == null) return true; if (p == null || q == null || p.val != q.val) return false; return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); }}Note: The traverseAndCheck recomputes hashes, which is inefficient. A better implementation would compute all hashes once and store them, but this illustrates the concept.
Complexity
Time
O(m + n) on average. We traverse `root` (`O(m)`) and `subRoot` (`O(n)`). The `isSameTree` check (`O(n)`) is only performed on a hash match. With a good hash function, collisions are rare, making the verification cost negligible. In the worst case of many collisions, it could degrade to `O(m * n)`.
Space
O(m + n) in the worst case for the recursion stack. The space for storing hashes is minimal.
Trade-offs
Pros
Very fast on average, with linear time complexity.
Avoids the overhead of large string creation, allocation, and searching.
Generally more space-efficient than string serialization as it only deals with numbers.
Cons
More complex to implement correctly than the other approaches.
Performance relies on a good hash function; a poor one could lead to many collisions, degrading performance to O(m * n).
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 isSubtree ( TreeNode root , TreeNode subRoot ) { if ( root == null ) { return false ; } return dfs ( root , subRoot ) || isSubtree ( root . left , subRoot ) || isSubtree ( root . right , subRoot ); } private boolean dfs ( TreeNode root1 , TreeNode root2 ) { if ( root1 == null && root2 == null ) { return true ; } if ( root1 == null || root2 == null ) { return false ; } return root1 . val == root2 . val && dfs ( root1 . left , root2 . left ) && dfs ( root1 . right , root2 . right ); } }/** * Definition for a binary tree node. * function TreeNode(val, left, right) { * this.val = (val===undefined ? 0 : val) * this.left = (left===undefined ? null : left) * this.right = (right===undefined ? null : right) * } */ /** * @param {TreeNode} root * @param {TreeNode} subRoot * @return {boolean} */ var isSubtree = function (root, subRoot) { if (!root) return false; let dfs = function (root1, root2) { if (!root1 && !root2) { return true; } if (!root1 || !root2) { return false; } return ( root1.val == root2.val && dfs(root1.left, root2.left) && dfs(root1.right, root2.right) ); }; return ( dfs(root, subRoot) || isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot) ); };/** * 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 isSubtree ( TreeNode * root , TreeNode * subRoot ) { if ( ! root ) return 0 ; return dfs ( root , subRoot ) || isSubtree ( root -> left , subRoot ) || isSubtree ( root -> right , subRoot ); } bool dfs ( TreeNode * root1 , TreeNode * root2 ) { if ( ! root1 && ! root2 ) return 1 ; if ( ! root1 || ! root2 ) return 0 ; return root1 -> val == root2 -> val && dfs ( root1 -> left , root2 -> left ) && dfs ( root1 -> right , root2 -> right ); } };# 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 isSubtree ( self , root : TreeNode , subRoot : TreeNode ) -> bool : def dfs ( root1 , root2 ): if root1 is None and root2 is None : return True if root1 is None or root2 is None : return False return ( root1 . val == root2 . val and dfs ( root1 . left , root2 . left ) and dfs ( root1 . right , root2 . right ) ) if root is None : return False return ( dfs ( root , subRoot ) or self . isSubtree ( root . left , subRoot ) or self . isSubtree ( root . right , subRoot ) )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.