Problem overview
The method isSameTree(_ p: TreeNode?, _ q: TreeNode?) -> Bool receives the roots of two binary trees and returns true when they are the same: the same shape, and the same value in every position. Two empty trees are the same. The lesson provides the TreeNode class, with val, left and right, and a missing tree or child is nil.
The trees [1, 2, 3] and [1, 2, 3] are the same. The trees [1, 2] and [1, nil, 2] hold the same values but are different: the 2 is a left child in the first and a right child in the second. The trees [1, 2, 1] and [1, 1, 2] have the same shape, but their left children hold 2 and 1.
Approach: compare the roots, then both pairs of 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. That is a definition you can code directly as recursion: the answer for a pair of trees depends on the answers for two smaller pairs.
Each call has three situations:
- Both trees are empty: they are the same, and the recursion ends.
- Exactly one is empty: the shapes differ, so the answer is
false. - Both have a node: if the values differ,
false. If they match, the trees are the same exactly when the left pair and the right pair are.
Every call compares one pair of nodes, and no pair is compared twice, so the time is O(n).
Step by step
- Handle two empty trees first: if
p == nil && q == nil, returntrue. This is also where the recursion stops when both sides step past a leaf at once. - Unwrap both at once:
guard let p = p, let q = q else { return false }. Theelsebranch runs when at least one isnil. Both being empty was handled above, so exactly one is empty and the shapes differ. - Compare the values: if
p.val != q.val, returnfalse. Both nodes exist here, so reading their values is safe. - Recurse on both pairs:
isSameTree(p.left, q.left) && isSameTree(p.right, q.right).
The solution in Swift
class Solution {
func isSameTree(_ p: TreeNode?, _ q: TreeNode?) -> Bool {
if p == nil && q == nil {
return true
}
guard let p = p, let q = q else {
return false
}
if p.val != q.val {
return false
}
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
}
}
The Swift details worth a closer look:
p == nilwithoutEquatable.TreeNodeis a class with no equality defined, but Swift lets you compare any optional withnilusing==and!=. It asks only whether the optional is empty.guard let p = p, let q = qunwraps two optionals in one statement. If either isnil, theelseblock runs. Below the guard,pandqare plainTreeNodevalues that hide the parameters of the same names. Theelseblock must leave the function, here withreturn false.- Short-circuit
&&. When the left pair is different, the right pair isn't examined. - Comparing values, not references. The code reads
val, so two separate trees with equal contents are the same. A tree compared with itself, passed as bothpandq, is also the same.
Complexity
Time: O(n), where n is the number of nodes in the smaller of the trees, at most, since the recursion ends as soon as the shapes differ. Each pair of nodes is compared once.
Space: O(h), where h is the height: the recursion goes one level deeper per node on the current path. The trees are at most 400 levels deep.
Edge cases and common mistakes
- Both empty.
[]and[]givetrue. - One empty.
[]and[1]givefalse. - Same values, different shape.
[1, 2]against[1, nil, 2]givesfalse: a check that compares only values in order would accept it. - A longer tree.
[1, 2]against[1, 2, 3]givesfalse: the second tree has a right child the first lacks. - Reading
.valbefore checking fornil. Accessing a value through a missing node would crash, which is why the order of the checks matters. - Value 0.
[0]against[0]givestrue: a value of zero is not an empty node.
Testing
[1, 2, 3]and[1, 2, 3]givetrue.[1, 2]and[1, nil, 2]givefalse.[1, 2, 1]and[1, 1, 2]givefalse.[]and[]givetrue.[]and[1]givefalse.[1]and[1]givetrue.[1, 2]and[1, 2, 3]givefalse.
