Problem overview
You get two binary trees, root and subRoot. The function isSubtree returns true when some node of root, taken together with all of its descendants, forms exactly the tree subRoot: the same shape and the same value in every position. A tree counts as a subtree of itself.
The lesson provides the TreeNode class, with an Int property value and two nullable children, left and right. An empty child is null.
Take root = [3,4,5,1,2] and subRoot = [4,1,2]. The node 4 in root has the children 1 and 2 and nothing below them, which is exactly subRoot, so the answer is true. Now give the 2 a child 0: root = [3,4,5,1,2,null,null,null,null,0]. The node 4 still looks right from the top, but its subtree is bigger than subRoot, and no other node fits, so the answer is false. Matching the top of the tree is not enough: everything below must match too.
Approach
Split the question in two:
- Given a node of
root, is the tree that starts there identical tosubRoot? - Which nodes of
rootshould we try?
The second question has a simple answer: all of them. If subRoot starts at the top of root, we are done. Otherwise it must be a subtree of the left child's tree or of the right child's tree, which is the same question on a smaller tree. That is a recursion over root.
The first question is also a recursion, over two trees at once. Two trees are identical when both are empty, or when both are non-empty, their root values are equal, their left subtrees are identical and their right subtrees are identical. That is a helper function, isSameTree.
The brute force here is already the intended solution: no cleverness is needed, only two small recursions that cooperate.
Step by step
The outer search, isSubtree
- Stop at an empty tree. If
rootisnull, returnfalse.subRoothas at least one node, and an empty tree has no node where it could start. This is also what ends the recursion when it steps below a leaf. - Try the current node. If
isSameTree(root, subRoot)istrue, returntrueat once. - Try lower down. Otherwise return
isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot). The||operator skips the right call if the left one already found a match.
The comparison, isSameTree
- Both empty. If
pandqare bothnull, the trees are identical: returntrue. This is the stopping case when both sides run past a leaf together. - Only one empty. Both empty was handled above, so if either is
nullhere, exactly one is. The shapes differ: returnfalse. - Different values. Both nodes exist now, so compare
p.valuewithq.valueand returnfalseif they differ. - Recurse. The roots match, so the trees are identical exactly when the left pair and the right pair are:
isSameTree(p.left, q.left) && isSameTree(p.right, q.right).
The solution in Kotlin
class Solution {
fun isSameTree(p: TreeNode?, q: TreeNode?): Boolean {
if (p == null && q == null) {
return true
}
if (p == null || q == null) {
return false
}
if (p.value != q.value) {
return false
}
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
}
fun isSubtree(root: TreeNode?, subRoot: TreeNode?): Boolean {
if (root == null) {
return false
}
if (isSameTree(root, subRoot)) {
return true
}
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot)
}
}
Kotlin details worth knowing
- Nullable parameters and smart casts. The parameters have the type
TreeNode?, so Kotlin won't let you readp.valueuntil it knowspisn'tnull. Afterif (p == null || q == null) { return false }it knows that both are non-null, sop.valueandq.valuecompile with no!!and no?.. Without that check you get:only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'TreeNode?'.The same happens inisSubtree: after theroot == nullcheck,root.leftis allowed. - Parameters are read-only. A function parameter can't be reassigned, which is why the smart cast is reliable here.
==againstnull. In Kotlin,p == nullis the normal null test; it's safe even for a nullablep.&&and||short-circuit. The right-hand side runs only when needed, which saves work but doesn't change the result.- The order of the checks matters. The "both null" test must come before the "either null" test. Swap them and two empty trees would be reported as different.
Complexity
Let n be the number of nodes in root and m the number in subRoot.
- Time: O(n · m).
isSubtreevisits each of thennodes ofrootat most once and starts one comparison at each. A comparison walks both trees in step and stops at the first difference, and it can never go past themnodes ofsubRoot(whensubRootis exhausted, a bigger tree below gives a mismatch). So each comparison costs at most O(m). - Space: O(h) for the recursion stack, where
his the larger of the two trees' heights. The constraints keep trees at most 400 levels deep, so the recursion stays shallow enough.
Edge cases and common mistakes
- Checking only the root values. Comparing the value at the top and stopping gives wrong answers. In the second example, node
4's children match, but the extra node0below the2makes the trees different. The comparison must go all the way down, and the empty children must match too. - Forgetting that a subtree includes every descendant. Matching a few levels of
rootand ignoring what hangs below is the same bug in another form. - Same value repeated. With
root = [1,1]andsubRoot = [1], the top node fails the comparison (it has a child) but its left child passes. Don't give up when the first candidate fails: the search has to continue below. - The tree itself.
subRootequal torootgivestrue, because the first comparison at the top already succeeds. - Treating
nullchildren as irrelevant. A missing child is part of the shape. A node with one child is different from a node with none.
Testing
Trees are written in level order, with null for a missing child. These inputs and results come from running the solution above:
root | subRoot | Result |
|---|---|---|
[3,4,5,1,2] | [4,1,2] | true |
[3,4,5,1,2,null,null,null,null,0] | [4,1,2] | false |
[1,1] | [1] | true |
[1,2,3] | [1,2,3] | true |
[1] | [2] | false |
[1,null,1,null,1,null,1] | [1,null,1] | true |
[3,4,5,1,2] | [4,1] | false |
The last row is a good one to think about: node 4 has children 1 and 2, while subRoot has only the child 1, so the shapes differ.
