Problem overview
The method isSameTree(p, q) receives the roots of two binary trees and returns true if they are the same tree. Two trees are the same when they have exactly the same shape and every node holds the same value as the node in the same place in the other tree. Two empty trees are the same. The lesson provides the TreeNode type: a node has a number val and two children, left and right, each a node or 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 in the first the 2 is the left child of the root and in the second it is the right child, so they differ. The trees [1,2,1] and [1,1,2] have the same shape, but their left children hold 2 and 1, so they differ too.
Approach: compare the roots, then the subtrees
A tree is made of a root and two smaller trees, and "the same" splits the same way: 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. That is a recursive definition, and it translates directly into a recursive function. The recursion ends when it reaches missing children, so there are three base cases to decide before comparing anything:
- both nodes are missing: nothing differs here, so the answer is
true; - exactly one is missing: the shapes differ, so the answer is
false; - both exist but hold different values:
false.
Only when all three tests are passed do the subtrees need to be compared. Each pair of nodes is looked at at most once, so the running time is linear.
Step by step
- Both empty. If
!p && !q, returntrue.!pis true whenpisnull. This test also ends the recursion when both trees run out at the same moment. - Only one empty. If
!p || !q, returnfalse. The case of two empty trees has already returned, so here exactly one tree is empty and the other is not. - Different values. If
p.val != q.val, returnfalse. Both nodes exist at this point, so readingvalis safe. - Compare the subtrees. 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 TypeScript
class Solution {
isSameTree(p: TreeNode | null, q: TreeNode | null): boolean {
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);
}
}
Details worth knowing:
- Truthiness of nodes. A
TreeNodeobject is always truthy, even when itsvalis 0, so!pis true only whenpisnull(orundefined). It tests the node, not its value. For two trees that each hold just a node with value 0, the function returnstrue, notfalse. !=and!==. The reference compares the values with!=. For two numbers it gives the same result as!==, because no conversion is needed.!=would convert if the types differed:1 != "1"isfalse, while1 !== "1"istrue. Withnumberfields that cannot arise, but!==is the safer habit.- Short-circuit
&&. When the left subtrees already differ, the right ones are not compared, since the answer is alreadyfalse. - The same tree twice. Passing a tree as both arguments works, and gives
true: the function never changes the trees. - Types are erased.
TreeNode | nullhelps the compiler and disappears in the JavaScript output. It checks nothing at run time.
Complexity
Time: O(n), where n is the number of nodes in the smaller tree, or in either when they are the same. Each pair of nodes is compared at most once, with constant work per pair, and the recursion stops at the first difference.
Space: O(h) for the recursion, where h is the height of the trees: up to the 400 levels the constraints allow, or O(log n) for balanced trees.
Edge cases and common mistakes
- Two empty trees.
[]and[]givetrue. One empty and one not, such as[]and[1], givesfalse. - Same values, different shape.
[1,2]against[1,null,2]isfalse. Comparing the values in level order or sorted order would miss this, which is why the left and right children are compared separately. - Same shape, different values.
[1,2,1]against[1,1,2]isfalse. - Different sizes.
[1,2,3]against[1,2]isfalse: when the recursion reaches the missing right child, only one side has a node. - Reading
valtoo early. Readingp.valbefore thenulltests would throw aTypeErroron a missing node. The order of the three tests matters. - Testing the value for truthiness. Writing something like
if (!p.val)would treat 0 as missing. Values can be 0 or negative, so the tests are on the nodes.
Testing
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 = [0],q = [0]givestrue.p = [1,2,3,4],q = [1,2,3,5]givesfalse.
