Problem overview
The function isSameTree receives the roots of two binary trees, p and q, and returns true when they are the same tree: the same shape, with the same value in every place. Two empty trees are the same. The lesson provides the TreeNode class, with an Int field value and nullable left and right children, and the parameters have the type TreeNode? because a tree may be empty.
The trees [1, 2, 3] and [1, 2, 3] are the same. The trees [1, 2] and [1, null, 2] hold the same values but are different: in the first, 2 is the left child of the root, and in the second it is the right child. The trees [1, 2, 1] and [1, 1, 2] have the same shape but different values in the left child.
Approach: compare the roots, then the subtrees
Two trees are the same when their roots hold the same value, their left subtrees are the same, and their right subtrees are the same. A subtree is itself a tree, so this is a recursive definition, and the code follows it directly. Each call compares one node of p with the node in the same place of q.
There are three ways for a pair of nodes to be decided without looking deeper:
- both are missing: the trees agree here, so the answer is
true; - exactly one is missing: the shapes differ, so the answer is
false; - both exist but hold different values: the answer is
false.
If none of these applies, the answer depends on the two pairs of children. Every node of the smaller tree is compared at most once, so the time is O(n).
Step by step
- Both empty. If
p == null && q == null, returntrue. This is also the base case that ends the recursion when both trees step past a leaf together. - Only one empty. If
p == null || q == null, returnfalse. The case of both being empty has already returned, so exactly one tree is missing a node here. - Different values. If
p.value != q.value, returnfalse. Both nodes exist at this point, so reading their values is safe. - Compare the children. Return
isSameTree(p.left, q.left) && isSameTree(p.right, q.right). The roots already match, so the trees are the same exactly when both pairs of subtrees are.
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)
}
}
The Kotlin details worth a closer look:
- Smart casts do the null checking. After
if (p == null || q == null) return false, Kotlin knows that neitherpnorqisnull, sop.valueandq.valuecompile without?.or!!. Without that early return, the compiler would rejectp.value. - The order of the checks matters. The "both null" test comes first. If the "one is null" test came first, two empty trees would be reported as different.
!=compares values. In Kotlin,==and!=callequals. ForIntfields they compare the numbers. The identity operator is===, which isn't needed here, since the trees are compared by content, not by whether they are the same object.- Short-circuit
&&. When the left subtrees differ, the right subtrees are not examined.
Complexity
Time: O(n), where n is the number of nodes in the smaller tree. Each call compares one pair of positions, and the recursion stops as soon as a difference is found.
Space: O(h), where h is the height of the trees, for the recursion stack. The trees are at most 400 levels deep.
Edge cases and common mistakes
- Two empty trees.
[]and[]givetrue. - One empty tree.
[]and[1]givefalse. - Same values, different shape.
[1, 2]and[1, null, 2]givefalse. Comparing only the values of the nodes, in any traversal that skips missing children, can't tell them apart. - Same shape, different values.
[1, 2, 1]and[1, 1, 2]givefalse. - One tree inside the other.
[1]and[1, 2]givefalse: the second has an extra node, so at the left child one side is missing. - The same object twice. Passing one tree as both
pandqgivestrue. - Swapping the checks. Testing
p.value != q.valuebefore the null checks fails to compile, becausepandqmay still benullthere.
Testing
Trees are written in level order, with null for a missing child.
p = [1, 2, 3],q = [1, 2, 3]givestrue.p = [1, 2],q = [1, null, 2]givesfalse.p = [1, 2, 1],q = [1, 1, 2]givesfalse.p = [],q = []givestrue.p = [],q = [1]givesfalse.p = [1],q = [1, 2]givesfalse.p = [7],q = [7]givestrue.
