Skip to content

Serialize Compare and Subtree

Two trees are the same when they have the same shape and the same values in the same places. That is a stricter condition than “the same set of values”, and getting the distinction right is what this family of problems tests.

The recursion is short. What makes these problems interesting is the bookkeeping:

  • Null markers. A traversal that omits empty children is ambiguous, so serialisation must record them explicitly.
  • Structural comparison. Comparing values without comparing shape accepts trees that are genuinely different.
  • Substring is not subtree. The tempting “serialise both and check containment” shortcut is wrong unless you add delimiters — and knowing why is the point of LC 572.

What you’ll learn

  • The parallel recursion for structural equality, and the order of its guards.
  • Why serialisation needs explicit null markers, with a concrete counter-example.
  • The subtree-matching trap, and how to fix the string shortcut.
  • Using serialisation as a hash key to find duplicate subtrees in O(n)O(n).
  • Three real LeetCode problems solved in the browser: 100, 572, 297.

The cue

Template 1 — structural equality

is_same_tree.py
def is_same_tree(p, q):
    if not p and not q:
        return True                    # both empty -> equal
    if not p or not q or p.val != q.val:
        return False                   # one empty, or values differ
    return (is_same_tree(p.left, q.left)
            and is_same_tree(p.right, q.right))
is_same_tree.py
def is_same_tree(p, q):
    if not p and not q:
        return True                    # both empty -> equal
    if not p or not q or p.val != q.val:
        return False                   # one empty, or values differ
    return (is_same_tree(p.left, q.left)
            and is_same_tree(p.right, q.right))

O(min(n,m))O(\min(n, m)) time — it stops at the first mismatch — and O(h)O(h) space.

Template 2 — serialisation with null markers

serialize.py
def serialize(root):
    out = []
 
    def dfs(node):
        if not node:
            out.append("#")            # EXPLICIT null marker
            return
        out.append(str(node.val))
        dfs(node.left)
        dfs(node.right)
 
    dfs(root)
    return ",".join(out)
 
 
def deserialize(data):
    tokens = iter(data.split(","))     # an iterator gives us "consume next"
 
    def dfs():
        token = next(tokens)
        if token == "#":
            return None
        node = TreeNode(int(token))
        node.left = dfs()              # the SAME order as serialisation
        node.right = dfs()
        return node
 
    return dfs()
serialize.py
def serialize(root):
    out = []
 
    def dfs(node):
        if not node:
            out.append("#")            # EXPLICIT null marker
            return
        out.append(str(node.val))
        dfs(node.left)
        dfs(node.right)
 
    dfs(root)
    return ",".join(out)
 
 
def deserialize(data):
    tokens = iter(data.split(","))     # an iterator gives us "consume next"
 
    def dfs():
        token = next(tokens)
        if token == "#":
            return None
        node = TreeNode(int(token))
        node.left = dfs()              # the SAME order as serialisation
        node.right = dfs()
        return node
 
    return dfs()

The iter(...)iter(...) plus next(...)next(...) idiom is the neat part of deserialisation: it gives you a stateful “consume the next token” without threading an index through the recursion. The recursion consumes tokens in exactly the order serialisation produced them, so no position arithmetic is needed at all.

TimeSpace
Same treeO(min(n,m))O(\min(n, m))O(h)O(h)
Serialize / deserializeO(n)O(n)O(n)O(n)
Subtree, naiveO(nm)O(n \cdot m)O(h)O(h)
Subtree, via serialisationO(n+m)O(n + m)O(n+m)O(n + m)
Duplicate subtreesO(n)O(n) average with hashingO(n)O(n)

The subtree trap

LC 572 asks whether subRootsubRoot appears as a subtree of rootroot. The natural solution tries same()same() at every node:

is_subtree.py
def is_subtree(root, sub_root):
    if not sub_root:
        return True
    if not root:
        return False
    return (same(root, sub_root)
            or is_subtree(root.left, sub_root)
            or is_subtree(root.right, sub_root))
is_subtree.py
def is_subtree(root, sub_root):
    if not sub_root:
        return True
    if not root:
        return False
    return (same(root, sub_root)
            or is_subtree(root.left, sub_root)
            or is_subtree(root.right, sub_root))

O(nm)O(n \cdot m) — fine at LeetCode’s constraints. But the tempting O(n+m)O(n + m) shortcut hides a real bug:

Serialisation as a hash key

Once a subtree has a canonical string, duplicate detection becomes a dictionary lookup. That is LC 652:

duplicate_subtrees.py
from collections import defaultdict
 
 
def find_duplicate_subtrees(root):
    counts = defaultdict(int)
    out = []
 
    def serialize(node):
        if not node:
            return "#"
        key = f"{node.val},{serialize(node.left)},{serialize(node.right)}"
        counts[key] += 1
        if counts[key] == 2:           # exactly 2 -> report once
            out.append(node)
        return key
 
    serialize(root)
    return out
duplicate_subtrees.py
from collections import defaultdict
 
 
def find_duplicate_subtrees(root):
    counts = defaultdict(int)
    out = []
 
    def serialize(node):
        if not node:
            return "#"
        key = f"{node.val},{serialize(node.left)},{serialize(node.right)}"
        counts[key] += 1
        if counts[key] == 2:           # exactly 2 -> report once
            out.append(node)
        return key
 
    serialize(root)
    return out

The == 2== 2 check reports each duplicate exactly once, however many copies exist. Note that building strings this way is O(n2)O(n^2) in the worst case because each key contains its children’s keys; assigning integer ids to distinct subtrees instead makes it a true O(n)O(n) — worth mentioning if pushed.

The variant map

VariantThe techniqueCanonical problem
Identical treesParallel recursion, three guards100
Mirror / symmetricCompare leftleft against rightright101 · 951
Subtree containmentsame()same() at every node, or delimited serialisation572
Serialize / deserializePreorder + null markers + a token iterator297
Duplicate subtreesSerialisation as a hash key652
Merge two treesParallel recursion building a new node617
Leaf sequence equalitySerialise only the leaves872

Practice — real LeetCode problems

LC 100 — Same Tree · Easy

Problem. Given the roots of two binary trees, return TrueTrue if they are structurally identical and every corresponding node has the same value.

Constraints. 0 <= number of nodes <= 1000 <= number of nodes <= 100, -10^4 <= Node.val <= 10^4-10^4 <= Node.val <= 10^4.

Examples. [1,2,3][1,2,3] and [1,2,3][1,2,3] give TrueTrue · [1,2][1,2] and [1,null,2][1,null,2] give FalseFalse (same values, different shape) · [1,2,1][1,2,1] and [1,1,2][1,1,2] give FalseFalse

Editorial — approach, complexity, follow-ups

Walk both trees together. At each step there are exactly three possibilities, and the code enumerates them in a safe order.

Time O(min(n,m))O(\min(n, m)) — short-circuits at the first difference. Space O(h)O(h).

The test cases isolate the distinct failure modes:

  • ([1,2], [1,null,2])([1,2], [1,null,2]) gives FalseFalse — identical multisets of values, different shape. This is why “compare sorted values” is not a valid solution.
  • ([1,2,1], [1,1,2])([1,2,1], [1,1,2]) gives FalseFalse — same shape, values in different positions.
  • ([], [])([], []) gives TrueTrue — two empty trees are equal.
  • ([1], [])([1], []) gives FalseFalse — the guard that would crash if ordered wrong.

Follow-ups you should expect: “Check if one is the mirror of the other (LC 101)?” — compare p.leftp.left with q.rightq.right and p.rightp.right with q.leftq.left. “Iteratively?” — push pairs onto a stack and compare as you pop. “Compare kk trees?” — pairwise against the first. “What if node values were floats?” — exact equality becomes unsafe; you would need a tolerance.

LC 572 — Subtree of Another Tree · Easy

Problem. Given the roots rootroot and subRootsubRoot, return TrueTrue if subRootsubRoot appears as a subtree of rootroot. A subtree consists of a node in rootroot and all of that node’s descendants.

Constraints. 1 <= root nodes <= 20001 <= root nodes <= 2000, 1 <= subRoot nodes <= 10001 <= subRoot nodes <= 1000, -10^4 <= Node.val <= 10^4-10^4 <= Node.val <= 10^4.

Examples. root = [3,4,5,1,2]root = [3,4,5,1,2], subRoot = [4,1,2]subRoot = [4,1,2] gives TrueTrue · root = [3,4,5,1,2,null,null,null,null,0]root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]subRoot = [4,1,2] gives FalseFalse (node 44’s subtree has an extra node 00)

Editorial — approach, complexity, follow-ups

Try an exact match at every node of rootroot. samesame requires the two trees to agree completely, all the way down — which is what enforces “and all of that node’s descendants”.

Time O(nm)O(n \cdot m) worst case: samesame is called at up to nn nodes and costs up to O(m)O(m) each. At the given constraints (2000 × 1000) that is acceptable, and it is the expected solution despite the problem’s “Easy” label. Space O(h)O(h).

Case 2 is the whole problem. Node 44 in rootroot has children 11 and 22, but 11 has an extra child 00. The values along the top match subRootsubRoot exactly, yet the subtree is different — so samesame must (and does) fail when it reaches 11’s children and finds 00 where subRootsubRoot has NoneNone.

Case 3, root = [1,1]root = [1,1] and subRoot = [1]subRoot = [1], is also instructive: the match succeeds at rootroot’s left child (a leaf 11), not at the root itself (whose subtree is [1,1][1,1]). It confirms you are trying every node.

Follow-ups you should expect: “Make it O(n+m)O(n + m)” — as above. “What if you only needed a matching subgraph, not a full subtree?” — a much harder tree pattern-matching problem. “Count occurrences instead?” — tally the matches rather than short-circuiting. “Duplicate subtrees within one tree (LC 652)?” — serialisation as a hash key.

LC 297 — Serialize and Deserialize Binary Tree · Hard

Problem. Design an algorithm to serialise a binary tree to a string and deserialise that string back to the identical tree. The format is up to you.

Constraints. 0 <= number of nodes <= 10^40 <= number of nodes <= 10^4, -1000 <= Node.val <= 1000-1000 <= Node.val <= 1000.

Example. [1,2,3,null,null,4,5][1,2,3,null,null,4,5] must round-trip back to the same tree.

Editorial — approach, complexity, follow-ups

Preorder with explicit null markers is self-delimiting: the recursion always knows when a subtree ends, because it hits a ##. So a single traversal suffices, unlike bare-traversal reconstruction which needs two.

Time O(n)O(n) both ways. Space O(n)O(n).

Three details worth stating:

  • Comma separation. Values can be multi-digit and negative (-1000 <= val <= 1000-1000 <= val <= 1000), so the string must be tokenised, not read character by character.
  • iteriter plus nextnext. This is what makes deserialisation clean. The recursion consumes tokens in the same order they were produced, so there is no index to thread through or mutate.
  • The empty tree. serialize(None)serialize(None) produces "#""#", and deserialising "#""#" returns NoneNone. The round trip works with no special case — worth checking, since [][] is a legal input.

[1,2][1,2] versus [1,null,2][1,null,2] are the pair that prove the markers are doing their job: they serialise to 1,2,#,#,#1,2,#,#,# and 1,#,2,#,#1,#,2,#,# respectively.

Follow-ups you should expect:

  • “Make it more compact.” For a BST (LC 449) you can drop the markers entirely — preorder alone determines a BST, so 1,2,31,2,3 suffices. That is a real saving and shows you noticed the extra structure.
  • “Use BFS instead.” Level-order with markers works too, and matches LeetCode’s own display format. Equally valid; preorder is usually shorter to write.
  • “What if values were strings that might contain commas?” You need escaping, or a length-prefixed format. A good practical question.
  • O(1)O(1) space?” Not possible — the output alone is O(n)O(n).
  • “Deep trees?” A 10^4-node chain exceeds Python’s recursion limit; raise it or serialise iteratively.

LeetCode problem set

#ProblemDifficultyThe twist
100Same TreeEasyThree guards in order; shape matters as much as values
101Symmetric TreeEasyCompare leftleft against rightright — mirrored recursion
572Subtree of Another TreeEasysame()same() at every node; the string shortcut needs delimiters and markers
617Merge Two Binary TreesEasyParallel recursion that builds a third tree
652Find Duplicate SubtreesMediumSerialisation as a hash key; report on count == 2== 2
297Serialize and Deserialize Binary TreeHardPreorder + null markers is self-delimiting
449Serialize and Deserialize BSTMediumA BST needs no markers — preorder alone determines it

Interview follow-ups

They askWhat they’re checkingThe answer
“Why null markers?”FoundationsWithout them preorder is ambiguous — [1,2][1,2] fits two trees. With them, one traversal determines the tree
“Why is substring containment wrong for subtrees?”Depth"12""12" contains "2""2"; you need per-value delimiters and null markers to block partial matches
“What’s the complexity of your subtree solution?”PrecisionO(nm)O(n \cdot m) naive; O(n+m)O(n + m) with delimited serialisation and substring search
“Compact the serialisation”Using structureFor a BST, drop the markers — preorder alone suffices (LC 449)
“Guard order in isSameTreeisSameTree?”CareBoth-NoneNone first, or you dereference NoneNone when the trees differ in size
“Find duplicate subtrees efficiently”CompositionSerialise each subtree as a hash key; assign integer ids for a true O(n)O(n)
“Values could contain your delimiter”Practical robustnessEscape them, or use a length-prefixed encoding

Edge-case checklist

  • Both trees emptyTrueTrue for LC 100; serialize(None)serialize(None) must round-trip.
  • One tree emptyFalseFalse; the guard-order test.
  • Same values, different shape[1,2][1,2] vs [1,null,2][1,null,2]; the reason shape must be compared.
  • Same shape, values transposed[1,2,1][1,2,1] vs [1,1,2][1,1,2].
  • Subtree matching at a non-root noderoot = [1,1]root = [1,1], subRoot = [1]subRoot = [1].
  • Extra descendants below a match — LC 572 case 2; a subtree must take all descendants.
  • Single-value trees with digit overlap[12][12] vs [2][2]; the string shortcut’s counter-example.
  • Negative and multi-digit values — comma separation is required.
  • Deep chain — 10^4 nodes exceeds Python’s recursion limit.

Recap

  • Tree equality is about shape and values together. Compare with a parallel recursion, and order the guards: both empty, then mismatch, then recurse.
  • Serialisation needs explicit null markers. With them, preorder alone determines the tree; without them it is ambiguous. Separate tokens with a delimiter so multi-digit and negative values survive.
  • iteriter plus nextnext makes deserialisation index-free — the recursion consumes tokens in the order they were written.
  • Substring is not subtree. The O(n+m)O(n+m) shortcut needs both per-value delimiters and null markers; [12][12] versus [2][2] is the counter-example.
  • A subtree includes all descendants — partial matches must fail.
  • Once subtrees have canonical strings, duplicate detection is a dictionary lookup (LC 652).
  • A BST needs no markers, because its ordering already encodes the split (LC 449).

Next: the recursion and backtracking patterns — subsets, permutations, and constrained search.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did