Blind 75 Visualized

#30 · Trees

Same Tree

Given the roots of two binary trees p and q, return true if they are structurally identical and every pair of corresponding nodes has the same value.

EasyLeetCode
  • Current node
  • On the path / call stack
  • Done / found
  • Rejected
  • Temporary thread (Morris)
step 1 / 1

Pointers

Variables

Hash map / table

Stack / queue

Foundations & data-structure selection

Binary tree (two at once)

Two trees are the same exactly when their roots hold equal values and their left subtrees are the same and their right subtrees are the same. That definition IS the recursion: walk both trees in lock-step.

Memory layout

Nodes are heap objects linked by pointers. Walking two trees touches 2n nodes; the extra memory is the recursion or stack depth, O(h).

OperationCost
compare the treesO(min(n, m)) time: stops at the first difference
extra spaceO(h) for recursion or an explicit stack

Lock-step traversal

Visit the same position in both trees at the same time and compare. Because both walks follow identical moves, a difference in shape shows up as one side being empty while the other is not.

Memory layout

A call stack of (p, q) pairs, or an explicit stack of node pairs.

OperationCost
pair comparisonO(1)

Why it fits

  • Equality of trees is defined recursively, so a recursive pair comparison mirrors the definition and is easy to prove correct.
  • It can stop at the first mismatch: two different trees are often rejected after looking at a few nodes, unlike serializing both (which always reads everything).
  • Checking "both empty" and "exactly one empty" explicitly makes the shape comparison part of the same logic as the values.

Why alternatives degrade

Compare only the in-order sequences:
Different shapes can share an in-order sequence (a chain leaning left and a chain leaning right), so values alone do not prove equality.
Serialize both without empty markers:
Without a marker for missing children, different trees can produce the same string.
Compare level counts only:
Two trees can have the same number of nodes per level but different shapes or values.

Four approaches, from naive to production

1 · beginner

Serialize both and compare

time O(n + m)space O(n + m)

Turn each tree into a string in pre-order (with "#" for missing children) and compare the strings.

bool isSameTree(TreeNode* p, TreeNode* q) {
    function<string(TreeNode*)> ser = [&](TreeNode* node) -> string {
        return node ? to_string(node->val) + "," + ser(node->left) + "," + ser(node->right) : "#";
    };
    return ser(p) == ser(q);
}

Flaws & bottlenecks

  • Always reads both trees completely, even when the roots already differ.
  • Allocates two strings (and many intermediate concatenations).
  • Needs the "#" marker, and a delimiter between numbers, or different trees collide (for example 1,23 versus 12,3).

Interviewer cross-questions

Why do you need the "#" marker?

Say: Without it the string forgets where a node has no child, so a tree with only a left child and one with only a right child could print the same text.

Why a delimiter between values?

Say: Without it the values 1, 23 and 12, 3 would both produce "123". A separator keeps the encoding unambiguous.

How can you avoid building strings?

Say: Compare the two trees node by node as you walk them: the recursive or iterative pair walk. It uses no extra memory beyond the stack and exits at the first mismatch.

2 · intermediate

Recursive pair comparison

time O(min(n, m))space O(h)

Both empty: same. One empty or different values: not the same. Otherwise the trees are the same when both left subtrees and both right subtrees are the same.

bool isSameTree(TreeNode* p, TreeNode* q) {
    if (!p && !q) return true;
    if (!p || !q || p->val != q->val) return false;
    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

Flaws & bottlenecks

  • Recursion depth equals the tree height: a chain of 100,000 nodes overflows the call stack.
  • The order of the three base checks matters: testing p.val before checking for null crashes.
  • The && short-circuit hides the order of evaluation, which is easy to forget when debugging.

Interviewer cross-questions

Why is the "both empty" check first?

Say: It is the only case that returns true without looking inside a node. The next check can then assume at least one node exists, so it only has to test "one is empty" and the values.

What is the complexity?

Say: Time O(min(n, m)): the walk stops where either tree ends or the first difference appears. Space O(h) for the recursion.

How would you make it safe for very deep trees?

Say: Use an explicit stack (iterative DFS) or BFS, so depth is limited by heap memory instead of the call stack.

3 · advanced

Iterative DFS with a stack of pairs

time O(min(n, m))space O(h)

Keep a stack of (node of p, node of q) pairs. Pop a pair, compare, and push the right then the left children as pairs.

bool isSameTree(TreeNode* p, TreeNode* q) {
    vector<pair<TreeNode*, TreeNode*>> stack = {{p, q}};
    while (!stack.empty()) {
        auto [a, b] = stack.back(); stack.pop_back();
        if (!a && !b) continue;
        if (!a || !b || a->val != b->val) return false;
        stack.push_back({a->right, b->right});
        stack.push_back({a->left, b->left});
    }
    return true;
}

Flaws & bottlenecks

  • Pushes pairs of empty children too, so about twice as many entries as there are nodes.
  • Allocates a tuple for every pair.
  • Slightly more code than recursion for the same behavior.

Interviewer cross-questions

Why skip a pair when both are empty (continue) instead of returning true?

Say: An empty-empty pair only says that position matches. Other pairs may still be waiting on the stack, so the loop has to continue.

Would BFS work as well?

Say: Yes. A queue of pairs gives the same O(n) time. BFS finds a difference near the root sooner; DFS uses less memory on bushy trees.

Why push right before left?

Say: So the left pair is popped first and the walk is a normal pre-order. Equality does not depend on the order.

4 · expert

Two parallel stacks, shape checked first

time O(min(n, m))space O(h)

Check the roots, then keep two stacks of nodes in lock-step. Before pushing children, compare their EXISTENCE, so only real nodes (never null) are pushed and every popped pair is guaranteed to have two nodes.

bool isSameTree(TreeNode* p, TreeNode* q) {
    if (!p || !q) return p == q;
    vector<TreeNode*> sa = {p};
    vector<TreeNode*> sb = {q};
    while (!sa.empty()) {
        TreeNode* a = sa.back(); sa.pop_back();
        TreeNode* b = sb.back(); sb.pop_back();
        if (a->val != b->val) return false;
        if (!a->left != !b->left || !a->right != !b->right) return false;
        if (a->right) { sa.push_back(a->right); sb.push_back(b->right); }
        if (a->left) { sa.push_back(a->left); sb.push_back(b->left); }
    }
    return true;
}

Flaws & bottlenecks

  • The existence check (!a.left !== !b.left) is a double negation that is hard to read.
  • Relies on the two stacks staying exactly in step: a bug in only one push desynchronizes them.
  • Non-null assertions (b.right!) hide a crash if the shape check is ever removed.

Interviewer cross-questions

Why check shape before pushing?

Say: If one node has a left child and the other does not, the trees already differ, so we can return at once. Otherwise every child we push has a partner, so the loop never meets a null and needs no null-pair case.

What does !a.left !== !b.left mean?

Say: !x turns a node-or-null into a boolean ("is empty"). Comparing the two booleans with !== is an XOR: true exactly when one child exists and the other does not.

Is it faster than the simple version?

Say: It does less work per node (no pairs of nulls) and exits earlier on shape mismatches, but the difference is small. Prefer the simple recursive version unless profiling says otherwise.

Synchronized code walkthrough

Showing: Serialize both and compare — shares the player above; click any step to jump.

bool isSameTree(TreeNode* p, TreeNode* q) {
    function<string(TreeNode*)> ser = [&](TreeNode* node) -> string {
        return node ? to_string(node->val) + "," + ser(node->left) + "," + ser(node->right) : "#";
    };
    return ser(p) == ser(q);
}