DSA sheet · Trees · DFS pattern

Subtree of Another Tree

This video takes the Same Tree check from Problem 1 and uses it as a tool inside a bigger search. We walk over every node of a big tree and ask, at each one: "does the small tree start right here?" Asking "is this the same tree?" at every node is the whole trick. The problem matters because it teaches a pattern you will see again: one recursive function that searches, calling another recursive function that checks.

Every problem below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Words we will use

The tool we reuse: Same Tree

The teacher assumes you already know Same Tree (Problem 1). Here is a quick recap, because the whole solution leans on it.

Same Tree (from Problem 1)
def isSame(p, q):
    if p is None and q is None:   # both empty: this spot matches
        return True
    if p is None or q is None:    # only one empty: shapes differ
        return False
    if p.val != q.val:            # both exist, numbers differ
        return False
    return isSame(p.left, q.left) and isSame(p.right, q.right)

Two fingers, one on each tree, moving together. Both None → fine. One None → shapes differ. Values differ → not same. Otherwise left-with-left and right-with-right.

Part A · Subtree of Another Tree with DFS

LeetCode 572

1The question in simple words

You get two trees: a big one (root) and a small one (subRoot). Return True if the small tree appears somewhere inside the big one, with the same shape and the same values. Otherwise return False.

"Appears inside" has a strict meaning: there must be some node X in the big tree where the subtree rooted at X is exactly the same tree as subRoot. That means: once X matches the top of subRoot, every child below X must match too, all the way down. X cannot have extra nodes hanging below it.

root (big tree)
        3
       / \
      4   5
     / \
    1   2
subRoot (small tree)
      4
     / \
    1   2
answer
True: the 4 on the left of
the root, with its 1 and 2,
is exactly subRoot.

And one case that looks right but is not (this one is in the LeetCode examples):

root
        3
       / \
      4   5
     / \
    1   2
       /
      0
subRoot
      4
     / \
    1   2
answer
False: under the big tree's 2
there is an extra 0. The
subtree at 4 is 4-1-2-0, which
is not the same as 4-1-2.

2What the constraints tell us

Doubt: the constraints say root has at least 1 node. So do I still need a "root is None" base case?
→ Yes. The constraint is only about the first call. Our search moves to root.left and root.right again and again, and sooner or later it walks off a leaf into None. Without a base case, the code would try to read None.val and crash. We will see this happen in Example 3 below.

3Intuition: how to think about it

Picture subRoot as a small stencil cut out of paper. You slide the stencil over the big tree, placing its top on one node at a time. At each spot you ask: "does the stencil fit exactly here?" That "fit exactly" question is just Same Tree.

Only the big tree's pointer moves during the search. subRoot stays fixed at its top. subRoot's own pointer starts moving only inside the Same Tree check, after a starting spot has been chosen.

So we need two functions:

functionjobwhich pointers move?
isSubtree(root, subRoot)Search: try every node of the big tree as a starting spot.only root moves (to left / right). subRoot stays put.
isSame(p, q)Check: does the tree at p exactly equal the tree at q?both move together (left-left, right-right).

4Building the conditions from examples

The teacher builds the logic on this example. To keep the two 4s apart, call them 4a (the upper one) and 4b (the lower one).

Example 1 · root
         3
        / \
      4a   5
     /  \
    1    4b
        /  \
       1    2
subRoot
      4
     / \
    1   2

Step 1: try the root first

Put the stencil on 3. Same Tree(3, 4) fails immediately: 3 ≠ 4. So the small tree does not start at 3. It must be somewhere in 3's left part or 3's right part.

Step 2: the root failed, so move only the big tree's pointer

Go to 3's left child, 4a. We change root to root.left, but subRoot stays at its top (4). Why? Because we haven't matched anything yet. We are still looking for a place to start, so the stencil's top must be compared again from the beginning.

Step 3: 4a matches the top, so now run the full Same Tree check

4a = 4, so this spot looks promising. Now Same Tree takes over, and here both pointers move together:

So the check started at 4a fails. Matching the top is not enough. Every node below must match too.

Step 4: keep searching below 4a

Back in the search. Try 4a's left child, 1: Same Tree(1, 4) fails at once (1 ≠ 4). Try 4a's right child, 4b: 4 = 4 ✓, left 1 = 1 ✓, right 2 = 2 ✓, and all their children are None on both sides ✓. Same Tree is True.

Step 5: found it, so stop and don't look at the right side

We found the small tree in the left part of 3. Is there any reason to visit 5 now? No. One match is enough. So the search should return True as soon as either side says True. That means we join the left search and the right search with or.

Doubt 1: in Same Tree we joined the two sides with and. Why or here?
→ They answer different questions. Same Tree asks "do all positions match?", so every side must be True → and. The search asks "is there at least one place where the stencil fits?", so one True side is enough → or. A bonus: Python's or skips the right call when the left already returned True. That is exactly the "don't visit 5" saving.

Rule: check the current node before going down

Suppose the big tree's root itself were 4 with children 1 and 2. Same Tree(root, subRoot) would be True right away. Why go left or right at all? So the very first thing the search does (after the base case) is: if isSame(root, subRoot): return True.

Search rule if isSame(root, subRoot): return True
return isSubtree(root.left, subRoot) or isSubtree(root.right, subRoot)
Only root changes in these calls. subRoot is passed exactly as it is.

The base case: what if the big tree's pointer becomes None?

The teacher compares two situations:

situationanswerwhy
root is None, subRoot is a real treeFalseWe're looking for a tree inside nothing. An empty spot can't contain real nodes.
root is a real tree, subRoot is NoneTrueAn empty tree is "inside" any tree. Every leaf has None children, so you can always find "nothing" somewhere.

The constraints say subRoot always has at least one node, and the search never changes subRoot. So the second row never happens. We only need the first: if root is None: return False.

Example 3: watching the base case fire

The teacher changes Example 1: suppose 4b has no children.

         3
        / \
      4a   5
     /  \
    1    4b        ← no children now
                   subRoot is still 4 → (1, 2)

Inside Same Tree: why each rule, in this problem

The teacher walks through Same Tree again, using the names root and subRoot:

Doubt 2: why do I need two separate functions? Can't one function do both jobs?
→ The two jobs move the pointers differently. The search keeps subRoot fixed and only moves the big tree. Same Tree moves both together. If you mix them, you end up matching "a piece of subRoot" with "a piece of root" at different depths, and you can get a wrong True. Keep the search and the check apart.

5Approach steps

  1. isSubtree(root, subRoot): if root is None → return False.
  2. If isSame(root, subRoot) is True → return True (found it right here).
  3. Otherwise search the left part, then the right part, keeping subRoot the same. Return True if either finds it.
  4. isSame(p, q): both None → True. One None → False. Values differ → False. Else return left-left and right-right.

6Code (Python)

Subtree of Another Tree with DFS
class Solution:
    def isSubtree(self, root, subRoot):
        if root is None:                          # walked off the big tree
            return False
        if self.isSame(root, subRoot):            # does it start right here?
            return True
        # not here: search left, then right; subRoot does NOT move
        return self.isSubtree(root.left, subRoot) or self.isSubtree(root.right, subRoot)

    def isSame(self, p, q):
        if p is None and q is None:               # both empty
            return True
        if p is None or q is None:                # only one empty
            return False
        if p.val != q.val:                        # values differ
            return False
        return self.isSame(p.left, q.left) and self.isSame(p.right, q.right)

7Code line by line

linewhat it means
if root is None: return FalseBase case of the search. The big tree's pointer has gone below a leaf. A real subRoot can't fit inside nothing.
if self.isSame(root, subRoot): return TruePut the stencil on the current node. If the whole subtree here equals subRoot, we are done. No need to go deeper.
self.isSubtree(root.left, subRoot)Not found here, so try every starting spot in the left part. Note: subRoot, not subRoot.left.
or self.isSubtree(root.right, subRoot)Then the right part, but only if the left part said False (or stops early on True).
isSame: the 3 ifsThe same three rules as Problem 1, in the same order, so .val is only read when both nodes exist.
return ... and ...The two trees are equal only if the left pair and the right pair are both equal.

8Dry run using the call stack

Example 1 again (4a on top, 4b below it). "S" = isSubtree, "same" = isSame.

  1. S(3): 3 isn't None. same(3, 4) → 3 ≠ 4 → False. So go left. S(3) waits.
  2. S(4a): same(4a, 4): 4 = 4 ✓ → same(1, 1): equal, children (N,N) and (N,N) → True. Then same(4b, 2): 4 ≠ 2 → False. True and False → same(4a, 4) is False. S(4a) goes left and waits.
  3. S(1): same(1, 4) → 1 ≠ 4 → False. Go left: S(None) → base case False. Go right: S(None) → False. False or False → S(1) returns False.
  4. Back in S(4a): left said False, so or runs the right side: S(4b).
  5. S(4b): same(4b, 4): 4 = 4 ✓, same(1, 1) ✓, same(2, 2) ✓ → True. S(4b) returns True right away, without going into its children.
  6. S(4a) gets True from its right side → returns True.
  7. S(3) gets True from its left side → or never calls S(5). Final answer: True ✓
step 2 (inside same)
S(3)S(4a)same(4a,4)same(4b,2) → False
step 3 (deepest search)
S(3)S(4a)S(1)S(None) → False
step 5
S(3)S(4a)S(4b) → True

The newest call is on top (red). Notice that a Same Tree check sits on top of the search calls while it runs. Both kinds of call use the stack at the same time.

9Complexity & remember

Time: the teacher calls it O(n²)

For every node of the big tree, we may run a Same Tree check that walks a big part of subRoot before it fails. The teacher's worst case makes this clear: all values are 1, except one 2 at the very bottom of subRoot.

root: all 1s
1
 \
  1
   \
    1
     \
      1
       \
        1  ...
subRoot
1
 \
  1
   \
    2

Start at the first 1: 1 = 1, 1 = 1, then 1 vs 2 fails. Go back and start at the next 1: again 1, 1, then fail. And again… At every starting node we walk almost all of subRoot before we find the mismatch. So the work is about (nodes in root) × (nodes in subRoot).

Space

Remember Subtree of Another Tree Search + check. Search: None → False · Same here → True · else left OR right (subRoot never moves). Check: plain Same Tree with AND. Time O(n·m), space O(h).

Part B · What about BFS?

The teacher doesn't code BFS here. She says it can be done, but she prefers DFS, and she leaves BFS to you as homework. Here's her reasoning, and then a working version so you can check your homework.

1Why the teacher prefers DFS here

Doubt: do I really need pairs in the queue?
→ For the search part, no. subRoot never moves, so a plain queue of big-tree nodes is enough. You pop a node, run Same Tree on it, and push its children. Pairs show up if you also write Same Tree with BFS, because then you pop nodes two at a time, as in Problem 1. The version below keeps it simple: a BFS search plus the DFS isSame.

2Approach steps

  1. Put the big tree's root in a queue (the constraints promise it exists).
  2. While the queue isn't empty: pop one node.
  3. If isSame(node, subRoot) → return True.
  4. Otherwise push its left and right children (only the ones that exist).
  5. If the queue runs out → no starting spot worked → return False.

3Code (Python)

Subtree of Another Tree, BFS search (homework answer)
from collections import deque

class Solution:
    def isSubtree(self, root, subRoot):
        queue = deque([root])
        while queue:
            node = queue.popleft()
            if self.isSame(node, subRoot):     # try this node as the start
                return True
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        return False

    def isSame(self, p, q):                    # same check as Part A
        if p is None and q is None:
            return True
        if p is None or q is None:
            return False
        if p.val != q.val:
            return False
        return self.isSame(p.left, q.left) and self.isSame(p.right, q.right)

4Dry run: watch the queue

Example 1. Yellow = the node popped at this step.

start3push the root
step 1pop 3 → same(3, 4)? 3 ≠ 4 ✗ → push 4a, 5
4a5
step 2pop 4a → 4 = 4, 1 = 1, but 4b ≠ 2 ✗ → push 1, 4b
514b
step 3pop 5 → 5 ≠ 4 ✗ → no children
14b
step 4pop 1 → 1 ≠ 4 ✗ → no children
4b
step 5pop 4b → same(4b, 4) ✓ → return True

Notice the difference from DFS: BFS checked 5 (step 3) before reaching 4b, because 5 is on a higher level. DFS never touched 5. Here DFS did less work, which matches the teacher's preference.

5Complexity


Part C · Revision page

isSubtree (search)isSame (check)
questiondoes subRoot start anywhere?are these two trees exactly equal?
None handlingroot None → Falseboth None → True · one None → False
pointers that moveonly root (subRoot fixed)both, together
combine children withor (one place is enough)and (every place must match)
early exitTrue as soon as foundFalse as soon as a mismatch
DFS (Part A)BFS search (Part B)
order of starting spotsdeep first: 3, 4a, 1, 4blevel by level: 3, 4a, 5, 1, 4b
pending work kept incall stackdeque
timeO(n × m)O(n × m)
spaceO(h): log n balanced, n skewedO(n)
teacher's pickpreferredworks, a bit messier
If you remember only 5 lines 1. Slide subRoot over every node of root like a stencil. "Fits exactly" = Same Tree.
2. Search: root None → False.
3. Search: if isSame(root, subRoot) → True. Else left or right.
4. In the search only root moves. subRoot stays at its top.
5. Worst case all 1s with a 2 at the bottom → O(n·m), still tiny for n = 2000.
Mistakes to avoid ✗ passing subRoot.left / subRoot.right in the search calls (subRoot must not move there)
✗ using and in the search (it should be or)
✗ using or in Same Tree (it should be and)
✗ thinking "the top value matches" means found (all children must match, no extra nodes)
✗ dropping the "root is None" base case because the constraints say n ≥ 1
test it yourself (paste under any of the solutions above)
root = TreeNode(3, TreeNode(4, TreeNode(1), TreeNode(4, TreeNode(1), TreeNode(2))), TreeNode(5))
sub  = TreeNode(4, TreeNode(1), TreeNode(2))
root2 = TreeNode(3, TreeNode(4, TreeNode(1), TreeNode(2, TreeNode(0))), TreeNode(5))
root3 = TreeNode(3, TreeNode(4, TreeNode(1), TreeNode(4)), TreeNode(5))

s = Solution()
print(s.isSubtree(root, sub))    # True  (found at the lower 4)
print(s.isSubtree(root2, sub))   # False (extra 0 under the 2)
print(s.isSubtree(root3, sub))   # False (lower 4 has no children)

Based on this video: Subtree of Another Tree | DFS