Problem overview
The method isSameTree(p, q) receives the roots of two binary trees and returns true when they are the same tree: the same shape, and the same value in every position. Two empty trees are the same. The lesson provides the TreeNode constructor, with val, left and right (both children start as null).
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, because the 2 is a left child in one and a right child in the other. 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. The second and third parts are the same question about smaller trees, so recursion fits. Each call looks at one pair of nodes, one from each tree, in the same position.
Three cases cover a pair of nodes:
- both are empty: they match, and the recursion stops;
- exactly one is empty: the shapes differ;
- both exist: their values must match, and then both pairs of children must match.
Each pair of nodes is compared at most once, so the time is O(n), where n is the number of nodes.
Step by step
- Both empty. If
!p && !q, returntrue.!pis true whenpisnull. This is also where the recursion ends when both trees run out at the same moment. - Only one empty. If
!p || !q, returnfalse. The first check already handled "both empty", so here exactly one of them is empty. - Different values. If
p.val != q.val, returnfalse. Both nodes exist at this point, so readingvalis safe. - Compare the children. Return
this.isSameTree(p.left, q.left) && this.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 JavaScript
class Solution {
isSameTree(p, q) {
if (!p && !q) {
return true;
}
if (!p || !q) {
return false;
}
if (p.val != q.val) {
return false;
}
return this.isSameTree(p.left, q.left) && this.isSameTree(p.right, q.right);
}
}
The JavaScript details worth knowing:
- Truthiness of nodes. A
nullchild is falsy and a node object is truthy, so!pis a short test for "no node". It doesn't depend on the node'sval: a node holding0is still truthy. !=against!==. The solution uses!=, which converts types before comparing, so0 != "0"isfalse. With node values that are always numbers, it behaves like!==. In your own code,!==is the safer habit, because it never converts.- Short-circuit
&&. When the left pair differs, the right pair is not compared, because the result is alreadyfalse. - Order of the checks. The "both empty" check must come first. If the "one is empty" test went first, two empty trees would be called different.
- Method calls. The recursion goes through
this.isSameTree, because the function is a method of the class.
Complexity
Time: O(n). Each pair of positions is compared at most once, and the work per pair is constant.
Space: O(h), the height of the trees, for the recursion stack. The constraints keep it within 400 levels.
Edge cases and common mistakes
- Two empty trees.
[]and[]givetrue. - One empty tree.
[]and[1]givefalse. - One tree is a prefix of the other.
[1]and[1, 2]givefalse: the roots match, but one pair of children is a node againstnull. - Value 0.
[0]and[0]givetrue. A test likeif (p.val)would treat 0 as missing; the solution checks the nodes, not the values. - Same values, different shape.
[1, 2]against[1, null, 2]givesfalse, because the left children are a node andnull. - Checking only the values. Comparing the lists of values, for example from a traversal that skips missing children, can't tell those two trees apart.
- Reading
valtoo early. Readingp.valwhenpisnullthrows aTypeError. The first two checks prevent that.
Testing
[1, 2, 3]and[1, 2, 3]givetrue.[1, 2]and[1, null, 2]givefalse.[1, 2, 1]and[1, 1, 2]givefalse.[]and[]givetrue.[]and[1]givefalse.[0]and[0]givetrue.
