Skip to content

Coding patternsBlind 75

Subtree of Another Tree

A subtree is the whole tree under one node, so compare subRoot with the tree under every node. Solved two ways, brute force in O(m × n) and pre-order lists with KMP search in O(m + n), with a step-through, code in five languages and the Big O worked out from the code.

You are given the roots of two binary trees, root and subRoot. Return true if root contains a subtree that is exactly the same as subRoot, and false if not.

A subtree is a node plus everything below it. So the copy must have the same values and the same shape, all the way down to the leaves. A tree also counts as a subtree of itself.

Trees in the examples are written level by level, from left to right. null marks a missing child.

Example 1

Input: root = [5, 9, 9, 1, null, 1, 4], subRoot = [9, 1, 4]

Output: true

The right 9 of root has children 1 and 4, exactly like subRoot. The left 9 has only the 1, so it is not a copy.

591914914

Example 2

Input: root = [2, 1, 3], subRoot = [2, 1]

Output: false

The tree under 2 also holds 3. A subtree must take everything below its node, so it is [2, 1, 3], not [2, 1].

Example 3

Input: root = [4, 4], subRoot = [4]

Output: true

The leaf 4 on its own is a subtree, and it equals subRoot.

  • root has 1 to 2,000 nodes. Call this m.
  • subRoot has 1 to 1,000 nodes. Call this n.
  • -10,000 ≤ Node.val ≤ 10,000
Must the copy reach down to the leaves?
Yes. A subtree takes a node and everything below it. A match that stops early, with more nodes under it in root, does not count.
Can values repeat?
Yes. That is why we cannot just find the value of subRoot once. Several nodes may start a near copy.
Can either tree be empty?
No. Both have at least one node, so there is always something to compare.
Does the whole tree count as a subtree of itself?
Yes. If root and subRoot are the same, the answer is true.

The question “is the tree under this node the same as subRoot?” is the Same Tree problem. We ask it at every node, which is a tree traversal. The faster way comes from turning both trees into lists and searching for one list inside the other, which is string matching.

This problem has two approaches. There is no useful step between them.

Optimal O(m + n)

Write each tree as a list in pre-order: the node, then its left subtree, then its right subtree. Write # for every empty spot. The # marks fix the shape, so two trees are equal exactly when their lists are equal.

A subtree is also one unbroken run of the pre-order list: pre-order writes a node and then everything below it, before moving on. So subRoot is a subtree of root exactly when the list of subRoot appears as one block inside the list of root.

Now it is a search for a pattern inside a text. KMP (Knuth-Morris-Pratt) does that without ever moving back in the text. It first builds a table fallback for the pattern: for each position, the length of the longest start of the pattern that also ends there. After a mismatch, fallback says how much of the match can still be kept.

Why it never misses the answer: the lists match exactly when the trees match, and KMP finds every place where the pattern appears in the text.

591914
text (root)
509112#3#4#59617#8#9410#11#12
  1. text = preorder(root) # "#" for each empty spot
  2. pattern = preorder(sub_root)
  3. fallback = fallback_table(pattern)
  4. matched = 0
  5. for value in text:
  6. while matched > 0 and value != pattern[matched]:
  7. matched = fallback[matched - 1]
  8. if value == pattern[matched]:
  9. matched += 1
  10. if matched == len(pattern):
  11. return True
  12. return False

Write root in pre-order: node, then left subtree, then right subtree. Every empty spot becomes #. That gives 13 items.

1 / 17
Solution.javasolution.pySolution.cssolution.gosolution.ts
class Solution {
// Pre-order lists with a marker for every empty spot, then KMP search.
// Building the lists is m + n steps, and KMP never moves backwards.
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
List<Integer> text = new ArrayList<>(), pattern = new ArrayList<>();
walk(root, text);
walk(subRoot, pattern);
// fallback[j]: the longest start of pattern that also ends at pattern[j].
// After a mismatch, KMP falls back to it instead of starting over.
int[] fallback = new int[pattern.size()];
for (int j = 1, prefixLength = 0; j < pattern.size(); j++) {
while (prefixLength > 0 && !Objects.equals(pattern.get(j), pattern.get(prefixLength)))
prefixLength = fallback[prefixLength - 1];
if (Objects.equals(pattern.get(j), pattern.get(prefixLength))) prefixLength++;
fallback[j] = prefixLength;
}
int matched = 0;
for (Integer value : text) {
// On a mismatch, keep the part of the match that can still grow.
while (matched > 0 && !Objects.equals(value, pattern.get(matched)))
matched = fallback[matched - 1];
if (Objects.equals(value, pattern.get(matched))) matched++;
// The whole pattern matched: a full copy of subRoot.
if (matched == pattern.size()) return true;
}
return false;
}
private void walk(TreeNode node, List<Integer> values) {
// The null markers fix the shape, so equal lists mean equal trees.
if (node == null) {
values.add(null);
return;
}
values.add(node.val);
walk(node.left, values);
walk(node.right, values);
}
}
Time O(m + n)Space O(m + n)Here is how we get these from the code, in four steps.
  1. 1 Find the work that repeats

    There are four parts: two pre-order walks, the fallback table and the scan. Each pre-order call writes one item. Each run of the scan reads one item, and each run of the inner while steps matched back once. Each of these is a fixed job, so each counts as 1 step.

    1. text = preorder(root) # "#" for each empty spot
    2. pattern = preorder(sub_root)
    3. fallback = fallback_table(pattern)
    4. matched = 0
    5. for value in text:
    6. while matched > 0 and value != pattern[matched]:
    7. matched = fallback[matched - 1]
    8. if value == pattern[matched]:
    9. matched += 1
    10. if matched == len(pattern):
    11. return True
    12. return False
  2. 2 Count the runs on Example 1

    The pre-order of root writes 13 items, one call each. The pre-order of subRoot writes 7. The fallback loop runs once for each item of the pattern after the first: 6 runs. The scan reads 13 items and falls back once, at the # where the pattern wanted 4.

    13 + 7 + 6 + 14 = 40 steps.

    partpreorder(root)preorder(sub)lps tablescan texttotal
    steps13761440
  3. 3 Write the count for any n

    A tree with m nodes has m + 1 empty spots, so its list has 2m + 1 items. The scan reads each text item once. The while can only step matched back as many times as matched went up, and matched goes up at most once per item. The fallback loop works the same way on the pattern. So:

    (2m + 1) + (2n + 1) + 2(2n + 1) + 2(2m + 1) = 6m + 6n + 6

    Check it with Example 1: this bound is 6 × 6 + 6 × 3 + 6 = 60. The real run used 40, because most items did not fall back.

  4. 4 Keep only the part that grows fastest

    • Drop the + 6. A fixed amount does not change the growth.
    • Drop the 6s. When m and n double, 6m + 6n doubles too.

    What is left is m + n.

    So the time is O(m + n).

  5. S Space: count the extra memory the same way

    The two lists hold 2m + 1 and 2n + 1 items, and fallback holds 2n + 1. The pre-order walks also use a call stack as deep as each tree. All of it grows with m + n, so the space is O(m + n).

    text (2m + 1 items)pattern (2n + 1 items)fallback (2n + 1 items)call stack

2,000 random pairs of trees: a root of 1 to 10 nodes, and a subRoot that is often cut out of it, sometimes with one value changed. The brute force and the optimal solution gave the same answer every time.

Largest inputsResult
2,000 nodes in root and 1,000 in subRoot (the most allowed), both one long line, every value 0 except the last node of subRoot. Optimal.0.001 s, and about 0.4 MB of extra memory: the two lists of values and the calls that wait during the walk, which is the O(m + n) space.
The same two lines. Brute force.0.2 s. At each of the 2,000 starting nodes it compares up to 1,000 nodes before the last one differs, about 1.5 million steps.