Problem overview
is_subtree receives two binary trees, root and sub_root, and answers whether sub_root appears inside root. "Appears" has a strict meaning here: some node of root, together with all of its descendants, must have exactly the same shape and the same values as sub_root. A tree counts as a subtree of itself.
In the first example, root is [3,4,5,1,2] and sub_root is [4,1,2]. The node 4 of root has the children 1 and 2, and nothing else below it, so the answer is true. In the second example the 2 has an extra child 0. The tree under node 4 is now bigger than sub_root, and no other node fits either, so the answer is false. In the third, root = [1,1] and sub_root = [1]: the root 1 has a child, so it doesn't match, but its left child 1 is a leaf and does.
Both trees arrive as &Option<Box<TreeNode>>. The lesson provides TreeNode, with the fields val, left and right, so you don't define it. The references matter: the search compares sub_root with many parts of root, so it must only borrow the trees.
Approach: compare at every node
The statement asks for O(n · m) time, where n and m are the sizes of the two trees. That suggests the simplest plan: treat each node of root as a possible starting point and ask "is the tree here the same as sub_root?". The question is a smaller problem of its own, comparing two trees for equality, and it has a natural recursive answer.
Two trees are the same when:
- both are empty, or
- both have a node, the two values are equal, and the left subtrees are the same, and the right subtrees are the same.
Anything else, including one empty tree and one non-empty tree, is a mismatch. With that helper, is_subtree is a second recursion: the answer is true if the trees are the same right here, or if sub_root is a subtree of the left child, or of the right child. An empty root contains nothing, since sub_root has at least one node.
The cost: one comparison starts at each of the n nodes of root, and a comparison can't go past the nodes of sub_root, so it stops after at most m steps. That is O(n · m).
Step by step
- Start with a
matchonroot. Its value, with no semicolon after the closing brace, is what the function returns. - The
Nonearm returnsfalse. An empty tree has no node wheresub_rootcould start. This is also where the recursion ends when it steps below a leaf. - The
Some(node)arm gets a block body. Becauserootis a reference,nodeis borrowed too, so readingnode.leftdoesn't move anything. - Compare here first. If
Self::is_same_tree(root, sub_root)is true, returntrueat once. - Otherwise search below. The arm ends with the left subtree check
||the right subtree check. The||skips the right side when the left side already found a match. - Write the helper
is_same_treeabove it, with two borrowed treespandq. - Match on the pair
(p, q)so one arm can look at both trees together. (None, None)istrue. Two empty trees are equal, and this ends the recursion when both sides pass a leaf at once.(Some(p), Some(q))rebindspandqto the nodes themselves, sop.valandp.leftwork directly.- Return
falseearly when the values differ. - End the arm with the two recursive calls joined by
&&: the left subtrees must match and so must the right ones. - The last arm,
_ => false, covers the case where exactly one tree is empty. Rust requires amatchto handle every case, and this arm completes it.
The solution in Rust
impl Solution {
fn is_same_tree(p: &Option<Box<TreeNode>>, q: &Option<Box<TreeNode>>) -> bool {
match (p, q) {
(None, None) => true,
(Some(p), Some(q)) => {
if p.val != q.val {
return false;
}
Self::is_same_tree(&p.left, &q.left) && Self::is_same_tree(&p.right, &q.right)
}
_ => false,
}
}
fn is_subtree(root: &Option<Box<TreeNode>>, sub_root: &Option<Box<TreeNode>>) -> bool {
match root {
None => false,
Some(node) => {
if Self::is_same_tree(root, sub_root) {
return true;
}
Self::is_subtree(&node.left, sub_root) || Self::is_subtree(&node.right, sub_root)
}
}
}
}
Rust details worth a closer look:
- Borrowing. Every parameter is a shared reference, so passing
sub_rootinto each recursive call copies only the reference. A value passed without&would move into the first call and couldn't be used again. - Matching on a reference.
match roothas type&Option<Box<TreeNode>>, yet the patterns are writtenNoneandSome(node). Rust matches through the reference and givesnodea borrow of the box, so nothing is moved out of the tree. - Shadowing. In
(Some(p), Some(q)), the newpandqhide the parameters of the same names inside that arm. They refer to the nodes, which is whyp.valcompiles. - Tuple patterns.
match (p, q)builds a pair of references and matches on both at once. The_arm catches the two mixed cases,(Some, None)and(None, Some). returnonly for the early exit. The final&&and||expressions are the arms' values, so they have no semicolon and noreturn.
Complexity
Time: O(n · m). is_subtree visits each node of root at most once, and each visit calls is_same_tree, which looks at no more than m nodes because it stops as soon as sub_root runs out or a value differs.
Space: O(h), where h is the larger depth of the two trees, for the recursion stack. The lesson limits trees to 400 levels, which is safe for recursion.
Edge cases and common mistakes
- Matching only the root. Checking
is_same_tree(root, sub_root)once would miss a match deeper down. The recursive calls on both children are what try every node. - Same values, different shape. A subtree takes every descendant. With
root = [3,4,5,1,2,None,None,None,None,0], node 4 matches[4,1,2]on its values, but the extra 0 below the 2 makes the trees different, so the result isfalse. - Forgetting the mixed case. When only one side is empty the trees differ. Without the
_ => falsearm the code wouldn't compile, and a wrong arm there would let a tree "match" a smaller one. - A tree is its own subtree. The root check happens before the recursion, so
is_subtree(&t, &t)istrue. - Single nodes. A leaf with value 5 against a leaf with value 5 gives
true; against a leaf with value 6,false. - A bigger
sub_root. A single node1against the tree1with a left child2givesfalse, because the comparison reaches a(None, Some)pair. - Mirror images. A tree with 2 on the right is not the same as one with 2 on the left, so
1with right child2is not a subtree of1with left child2. Left is compared with left only.
Testing
Running the reference solution on these trees prints:
root = [3,4,5,1,2],sub_root = [4,1,2]givestrue.root = [3,4,5,1,2,None,None,None,None,0],sub_root = [4,1,2]givesfalse.root = [1,1],sub_root = [1]givestrue.root = [3,4,5,1,2]against itself givestrue.root = [1],sub_root = [1,2]givesfalse.root = [5],sub_root = [6]givesfalse.- A chain of 400 nodes, each the left child of the next, all with value 1, against
[2]givesfalse.
