DSA sheet · Trees · DFS & BFS pattern

Same Tree & Symmetric Tree

These two problems are taught together because they use the same idea. If you understand Same Tree properly, Symmetric Tree needs only one small change. The teacher solves both twice: first with DFS (recursion), then with BFS (a queue), so we learn to think in both styles.

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

What is a tree node?

Each node is a small box with 3 things: its value, a link to its left child and a link to its right child. If a child is missing, the link is None (nothing there).

given by LeetCode, don't write this in the solution
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val       # the number in this node
        self.left = left     # left child, or None
        self.right = right   # right child, or None

Two ways to visit every node

DFS: Depth First SearchBFS: Breadth First Search
IdeaGo deep down one path first. When you reach the end, come back and try the other side.Go level by level: first the root, then all nodes on level 1, then level 2…
How we code itRecursion: a function that calls itself for the left child and the right child.A queue (collections.deque): add at the back, remove from the front.
Who remembers the pending work?Python's call stackThe queue

Why read constraints first? (the teacher always does this)

Constraints are not decoration. They answer 3 questions before you write any code:

  1. Do I need a base case? If the number of nodes can be 0, the root itself can be None, so the code must handle that.
  2. int or long? Are the values small enough for a normal integer? (In Python ints never overflow, but in Java/C++ this matters.)
  3. Will my code be fast enough (no TLE)? Roughly, more than about 10⁸ operations gives TLE (Time Limit Exceeded). If n is tiny, even O(n²) or O(n³) is fine.

Part A · Same Tree with DFS

LeetCode 100

1The question in simple words

You are given the roots of two binary trees, p and q. Tell whether they are the same tree.

Two trees are "the same" only when both of these are true:

2What the constraints tell us

3Intuition: how to think about it

Imagine you put one finger on each tree, both at the root. You move the two fingers together, always to the same position: if one goes left, the other also goes left.

At every position you ask a few questions: "Is there a node under both fingers? Are their values equal?"

Moving the fingers "go left fully, come back, then go right" is exactly DFS. In code, the "two fingers" are the two parameters of our function: p (finger on tree 1) and q (finger on tree 2).

4Building the conditions from examples

The teacher doesn't give the rules directly. She walks through 3 examples and discovers one rule from each. Let's do the same.

Example 1
  tree p        tree q
     1             1
    / \           / \
   2   3         2   3
  / \           / \
 4   5         4   5
Example 2
  tree p        tree q
     1             1
    / \           / \
   2   3         2   4
  / \           / \
 4   5         4   5
Example 3
  tree p        tree q
     1             1
    / \           /
   2   5         2     ← missing
  /             /
 4             4

From Example 1 → Rule: both are None → True

Walk the fingers: root 1 and 1 → exist, equal ✓. Left 2 and 2 ✓. Left 4 and 4 ✓. Right 5 and 5 ✓. Right side 3 and 3 ✓. So they are the same tree.

Now look at node 3. It has no left child in either tree. When the fingers go to 3's left, both land on None. Nothing is there and nothing is missing, so this position agrees. There is nothing more to check below it, because there is no node.

Rule 1if p is None and q is None: return True
Both empty means this spot matches. We don't need to look at values, because there are no values.

From Example 2 → Rule: both exist but values differ → False

Everything matches until the right side of the root: tree p has 3, tree q has 4. Both nodes exist, but the values are not equal. One mismatch is enough to say "not same".

Rule 3 (we'll see why it's 3rd in a moment)if p.val != q.val: return False

From Example 3 → Rule: only one of them is None → False

At the right side of the root, tree p has node 5, but tree q has nothing. One finger is on a node, the other is on None. The shapes are different, so the answer is False. It can also be the other way round (p is None, q has a node). Both cases are False.

Rule 2if p is None or q is None: return False
Doubt 1: "one is None" means (p is None and q is not None) or (p is not None and q is None). Why is a simple p is None or q is None enough?
→ Because Rule 1 runs first. If both were None, we already returned True and never reach this line. So if we reach Rule 2, we know at least one node exists. Now:
• if p is None is true, then q must be the one that exists → only one is None → False ✓
• if p is not None, Python checks q is None. If that's true, p exists and q doesn't → False ✓
So one or catches both "p missing" and "q missing".
Doubt 2: for Rule 3, do I need to check "both exist" again before reading .val?
→ No. If Rule 1 didn't fire (not both None) and Rule 2 didn't fire (neither is None), then both must exist. So p.val and q.val are safe to read. This is why the order of the rules matters: 1 → 2 → 3.

The last situation → both exist and values are equal → check the children

If all 3 rules passed, this position is fine. That doesn't mean the whole tree is same yet. We still have to check what's below. This is where DFS comes in: we call the same function again for the children.

Doubt 3: why send p.left and q.left together in one call?
→ Because the next question is "are the left child of p and the left child of q the same?" The function can only compare two nodes if it gets both of them. Both fingers move left together.

What do we return after the two calls? AND, not OR

Each call gives back True or False. Should the tree be "same" if either side is same, or only if both sides are same?

Think of Example 2: the left side (2-4-5) is same → True, the right side (3 vs 4) → False. The trees are clearly not same. So we need both to be True → use and.

left resultright resultleft and rightmeaning
TrueTrueTrueboth sides match → same
TrueFalseFalseright side broke it (Example 2)
FalseTrueFalseleft side broke it
FalseFalseFalseboth broke

5Approach steps (the algorithm in plain English)

  1. Take two nodes p and q (same position in the two trees).
  2. If both are None → return True.
  3. If only one is None → return False.
  4. If the values differ → return False.
  5. Otherwise check left children together and right children together (recursively).
  6. Return True only if both the left check and the right check are True.

6Code (Python)

Same Tree with DFS
class Solution:
    def isSameTree(self, p, q):
        if p is None and q is None:      # Rule 1
            return True
        if p is None or q is None:       # Rule 2
            return False
        if p.val != q.val:               # Rule 3
            return False
        left = self.isSameTree(p.left, q.left)       # left with left
        right = self.isSameTree(p.right, q.right)    # right with right
        return left and right

Shorter way, same meaning: replace the last 3 lines with return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right). A bonus of the one-line version: if the left side is already False, Python doesn't even run the right call.

7Code line by line

linewhat it means
if p is None and q is None: return TrueBoth fingers are on empty spots. Nothing to compare, so this spot matches. This is also our base case: it stops the recursion at the bottom of the tree, and it handles the "0 nodes" case.
if p is None or q is None: return FalseExactly one is empty (we know both aren't, from the line above). The shapes differ.
if p.val != q.val: return FalseBoth nodes exist (safe to read .val), but the numbers differ.
left = self.isSameTree(p.left, q.left)Ask the same question for the left children. Python pauses here, finishes the whole left side, then comes back.
right = self.isSameTree(p.right, q.right)Then ask for the right children.
return left and rightThis position is "same" only if both sides below it are same.

8Dry run using the call stack

The teacher's dry run uses Example 1, but with q's right node changed from 3 to 4 (that's Example 2). "N" = None. Read it top to bottom like a story:

  1. Call (1, 1): both exist, 1 = 1 ✓ → go left first. (1,1) waits on the stack.
  2. Call (2, 2): ✓ → go left. (2,2) waits.
  3. Call (4, 4): ✓ → go left.
  4. Call (N, N), the left children of 4: both None → True. This call is finished and removed from the stack.
  5. Back in (4,4), go right: call (N, N) → True.
  6. (4,4) now has left = True, right = True → returns True to (2,2) and leaves the stack.
  7. (2,2) goes right: call (5, 5) ✓ → its two children are (N,N) and (N,N) → True, True → (5,5) returns True.
  8. (2,2) has left = True, right = True → returns True to (1,1).
  9. (1,1) goes right: call (3, 4): both exist, but 3 ≠ 4 → False (Rule 3).
  10. (1,1) has left = True, right = False → True and False = False. The stack is now empty. Final answer: False ✓
stack at step 4 (deepest)
(1,1)(2,2)(4,4)(N,N) → True
stack at step 7
(1,1)(2,2)(5,5)
stack at step 9
(1,1)(3,4) → False

The newest call is on top (red). A call leaves the stack as soon as it returns its answer to the call below it.

9Complexity & remember

Remember Same Tree Two fingers move together. Rules in order: both None → True · one None → False · values differ → False · else check left-left AND right-right.

Part B · Symmetric Tree with DFS

LeetCode 101

1The question in simple words

Now you get only one tree. Check whether it is symmetric, meaning a mirror image of itself. Imagine folding the paper along a vertical line through the root: the left half should land exactly on the right half.

Symmetric ✓
        1
      /   \
     2     2
    / \   / \
   3   4 4   3
Not symmetric ✗
        1
      /   \
     2     2
      \     \
       3     3

In a mirror, the left side of one half must match the right side of the other half. In the first tree: the left 2's left child (3) matches the right 2's right child (3). The left 2's right child (4) matches the right 2's left child (4).

2What the constraints tell us

3Intuition: why this is almost the same as Same Tree

Cut the tree at the root. You now have two smaller trees: the left subtree and the right subtree. The question becomes: are these two trees mirror images of each other?

That is just like Same Tree, comparing two trees with two fingers. The only difference is the direction the fingers move:

Same TreeSymmetric Tree (mirror)
when finger 1 goes left…finger 2 goes leftfinger 2 goes right
when finger 1 goes right…finger 2 goes rightfinger 2 goes left
calls(p.left, q.left), (p.right, q.right)(p.left, q.right), (p.right, q.left)

4The conditions (same rules, new examples)

The teacher checks that every rule from Same Tree still holds here:

All the rules are copied. Only the two recursive calls change. That's why the teacher says "only these two parts of the code need to change, we just swap them".

5How do we start with only one tree?

Our helper function needs two nodes, but we have one root. The teacher's trick: write a helper isMirror(p, q) and call it with the same root twice: isMirror(root, root).

Doubt: can I directly start with isMirror(root.left, root.right)?
→ Yes, that also works and skips the useless "root vs root" step. The teacher mentions this option in the BFS video. Both are correct.

6Approach steps

  1. Call isMirror(root, root).
  2. Inside: both None → True; one None → False; values differ → False.
  3. Otherwise return isMirror(p.left, q.right) and isMirror(p.right, q.left).

7Code (Python)

Symmetric Tree with DFS
class Solution:
    def isSymmetric(self, root):
        if root is None:                 # safe guard (constraints say root exists)
            return True
        return self.isMirror(root, root)

    def isMirror(self, p, q):
        if p is None and q is None:      # Rule 1 (same as Same Tree)
            return True
        if p is None or q is None:       # Rule 2
            return False
        if p.val != q.val:               # Rule 3
            return False
        # ONLY CHANGE: mirror -> left with right, right with left
        return self.isMirror(p.left, q.right) and self.isMirror(p.right, q.left)
linewhat it means
return self.isMirror(root, root)Start both fingers on the root. The helper does all the work.
first 3 ifs in isMirrorExactly the same 3 rules as Same Tree, for the same reasons.
self.isMirror(p.left, q.right)The outer pair: the far-left side must mirror the far-right side.
self.isMirror(p.right, q.left)The inner pair: the two sides near the middle line must mirror each other.
… and …Both pairs must be mirrors for the whole thing to be symmetric.
Common mistakeCopying Same Tree and forgetting to swap the calls, so it stays (p.left, q.left). The teacher did this in the video too, the code failed, and she fixed it by swapping. If your Symmetric answer is wrong, check this first.

8Dry run

        1
      /   \
    2a     2b          (2a = left 2, 2b = right 2)
   /  \      \
  3    4      3        ← 2b has NO left child
  1. isMirror(1, 1): same node, equal ✓ → first check (1.left, 1.right) = (2a, 2b).
  2. (2a, 2b): both exist, 2 = 2 ✓ → outer pair first: (2a.left, 2b.right) = (3, 3).
  3. (3, 3): ✓ → its pairs (N, N) and (N, N) → True, True → returns True.
  4. Back in (2a, 2b): inner pair (2a.right, 2b.left) = (4, N) → only one is None → False.
  5. (2a, 2b) = True and False = False.
  6. Back in (1,1): the first part is already False, so and stops right there. Final answer: False ✓ The 4 has no mirror partner.

9Complexity & remember

Remember Symmetric TreeSame Tree code + swap the calls: (p.left, q.right) and (p.right, q.left). Start with (root, root).

Part C · Same Tree with BFS

Same question as Part A (two trees, are they same?), but now we check level by level using a queue instead of recursion. The rules don't change. Only the way we move through the tree changes.

1Recall normal BFS (level order)

plain BFS on ONE tree, just to remember the shape
from collections import deque
queue = deque([root])          # 1. put the root in the queue
while queue:                   # 2. until the queue is empty
    node = queue.popleft()     # 3. take ONE node from the front
    # ... do something with node (print / add) ...
    if node.left:  queue.append(node.left)    # 4. add children
    if node.right: queue.append(node.right)

2Intuition: what changes when we have two trees?

3The conditions, and the 3 important points she explains

Point 1: push None children too (don't skip them)

In normal BFS we only push a child if it exists. Here we push it even if it's None. Why?

Suppose tree 1 has a left child and tree 2 doesn't. If we skip the None, we push only tree 1's child. Later we pop two, but the second one isn't its partner (or the queue is empty, and the pop crashes). The pairs get mixed up.

If we push the None, the pair stays together as (node, None), and Rule 2 catches it → False ✓.

Doubt: can I skip the None children to save space?
→ You can, but then you can't pop two nodes back to back. After every pop you'd have to check "is the queue empty?" and change all the conditions. Pushing None keeps the checks exactly like the DFS version, which is much simpler. The cost is extra None entries in the queue. The bottom level has the most of them, and with L levels there can be about 2L slots down there.

Point 2: both None → continue, NOT return True

This is the most important difference from DFS.

So when both are None, this pair is fine. We just skip it and move to the next pair → continue.

Point 3: one None, or values differ → return False immediately

Here returning right away is correct. One mismatch anywhere means the trees aren't same, so there's no need to check the rest.

Bonus points

4Approach steps

  1. Make a queue and push p and q.
  2. While the queue is not empty: pop two nodes n1, n2.
  3. Both None → continue.
  4. One None → return False.
  5. Values differ → return False.
  6. Push n1.left, n2.left, n1.right, n2.right (None included).
  7. After the loop → return True.

5Code (Python)

Same Tree with BFS
from collections import deque

class Solution:
    def isSameTree(self, p, q):
        queue = deque()
        queue.append(p)
        queue.append(q)

        while queue:
            n1 = queue.popleft()
            n2 = queue.popleft()

            if n1 is None and n2 is None:
                continue
            if n1 is None or n2 is None:
                return False
            if n1.val != n2.val:
                return False

            queue.append(n1.left)
            queue.append(n2.left)
            queue.append(n1.right)
            queue.append(n2.right)

        return True

6Code line by line

linewhat it means
queue.append(p) queue.append(q)Start with the first pair: the two roots (even if they're None).
while queue:Keep going while there are pairs left to check.
n1 = queue.popleft() n2 = queue.popleft()Take out one pair: n1 from tree 1, n2 from the same position in tree 2.
if n1 is None and n2 is None: continueThis pair is fine, nothing to compare. Skip to the next pair. (Not return True!)
if n1 is None or n2 is None: return FalseShapes differ, so we can stop right now.
if n1.val != n2.val: return FalseValues differ, so we can stop right now.
append n1.left, n2.leftLeft children go in as a pair.
append n1.right, n2.rightRight children go in as a pair.
return TrueEvery pair passed, so the trees are the same.

7Dry run: watch the queue

Using Example 1 (both trees: 1 → 2, 3 → 2 has 4, 5). Yellow = the two we pop at this step. N = None.

start11push the two roots
step 1pop 1, 1 → both exist, equal ✓ → push 2, 2, 3, 3
2233
step 2pop 2, 2 ✓ → push 4, 4, 5, 5
334455
step 3pop 3, 3 ✓ → 3 has no children → push N, N, N, N
4455NNNN
steps 4–5pop 4,4 ✓ and 5,5 ✓ → each pushes N, N, N, N
steps 6+only None pairs left → each one: both None → continue
endqueue empty → loop ends → return True ✓

Notice the order we checked: 1 → 2 → 3 → 4 → 5. Level 0, then level 1, then level 2. That's BFS.

Why continue matters: the wrong version on a tricky example

  tree p        tree q
     1             1
    / \           / \
   2   3         2   3
      /             /
     5             6
step 1pop 1,1 ✓ → queue: 2, 2, 3, 3
step 2pop 2,2 ✓ (no children) → queue: 3, 3, N, N, N, N
step 3pop 3,3 ✓ → queue: N, N, N, N, 5, 6, N, N
step 4pop N, N →
with return True: the function ends and says "same" ✗. The 5 vs 6 pair was never checked!
with continue: keep going → pop N,N → continue → pop 5,6 → 5 ≠ 6 → return False ✓

8Complexity & DFS vs BFS

Which is better, DFS or BFS? For this problem, both are O(n) time and O(n) space, so neither wins on paper. The teacher's advice: for some problems DFS is easier, for others BFS is. So for every tree problem, practise thinking of both, then pick the one that's easier or faster.


Part D · Symmetric Tree with BFS

1Intuition: what changes from Part C?

Take the BFS Same Tree code and change just 2 things:

  1. Starting pair: there's only one tree now, so push root, root (the same trick as in DFS). Or push root.left, root.right directly. The teacher shows that both work.
  2. Mirror pairing when pushing children: push n1.left next to n2.right, and n1.right next to n2.left. Because we pop two at a time, the mirror partners come out together.

Everything else is identical: pop two, continue on both None, False on one None or different values, True at the end.

The constraints say at least 1 node, so the root exists and we don't need an empty check here.

2Code (Python)

Symmetric Tree with BFS
from collections import deque

class Solution:
    def isSymmetric(self, root):
        queue = deque()
        queue.append(root.left)        # the left half...
        queue.append(root.right)       # ...vs the right half

        while queue:
            n1 = queue.popleft()
            n2 = queue.popleft()

            if n1 is None and n2 is None:
                continue
            if n1 is None or n2 is None:
                return False
            if n1.val != n2.val:
                return False

            queue.append(n1.left)      # outer pair
            queue.append(n2.right)
            queue.append(n1.right)     # inner pair
            queue.append(n2.left)

        return True

3Dry run

        1
      /   \
    2a     2b
   / \     / \
  3   4   4   3
start2a2broot.left, root.right
step 1pop 2a, 2b → 2 = 2 ✓ → push 2a.left(3), 2b.right(3), 2a.right(4), 2b.left(4)
3344
step 2pop 3, 3 ✓ → push N, N, N, N
step 3pop 4, 4 ✓ → push N, N, N, N
steps 4–74 pairs of (N, N) → continue each time
endqueue empty → return True ✓ symmetric

If the bottom row were 3 4 3 4 instead, step 1 would push (3, 4, 4, 3). Step 2 would pop 3 and 4 → different → return False.

4Complexity & why BFS ran slower

O(n) time, O(n) space, the same as DFS. But on LeetCode the BFS version ran a bit slower. The teacher's reason: BFS does many enqueue and dequeue operations, including all the None entries. DFS only keeps one chain of calls on the stack at a time.


Part E · Revision page

DFS (recursion)BFS (queue)
moves through the treedeep first, then comes backlevel by level
pending work is kept inthe call stacka deque
startcall f(p, q) / f(root, root)push p, q / root, root (or root.left, root.right)
both Nonereturn True (goes back to the parent call)continue (return would end everything)
one None / values differreturn Falsereturn False
childrentwo recursive calls joined with andpush 4 children in pairs, None included
endresult of left and rightreturn True after the loop
time / spaceO(n) / O(n)O(n) / O(n)
Same TreeSymmetric Tree
input2 trees1 tree (compare it with itself)
pairsleft-left, right-rightleft-right, right-left
rulesidentical: both None ✓ · one None ✗ · values differ ✗
If you remember only 5 lines 1. Two fingers, one on each tree, always moving together.
2. Rules in order: both None → OK · one None → False · values differ → False.
3. Same Tree pairs L-L and R-R. Symmetric pairs L-R and R-L. That's the only change.
4. DFS: combine with and. BFS: pop two at a time, push None too.
5. In BFS, "both None" means continue, never return True.
Mistakes to avoid ✗ using or to combine left/right (must be and)
✗ checking .val before the None checks (crash)
✗ forgetting to swap the calls in Symmetric
✗ return True on both None in BFS
✗ skipping None children in BFS (the pairs break)
test it yourself (paste under any of the solutions above)
t1 = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
t2 = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
t3 = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(4))
sym = TreeNode(1, TreeNode(2, TreeNode(3), TreeNode(4)), TreeNode(2, TreeNode(4), TreeNode(3)))
not_sym = TreeNode(1, TreeNode(2, TreeNode(3), TreeNode(4)), TreeNode(2, None, TreeNode(3)))

s = Solution()
print(s.isSameTree(t1, t2))     # True
print(s.isSameTree(t1, t3))     # False
print(s.isSymmetric(sym))       # True
print(s.isSymmetric(not_sym))   # False

Based on these videos: Same Tree & Symmetric Tree | DFS · Same Tree & Symmetric Tree Using BFS