Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 11 – Tree patterns: depth-first

Serialise trees to find duplicate subtrees

Serialise a binary tree with null markers and load it back, check whether one tree is a subtree of another, and find duplicate subtrees in O(n) with IDs.

  • Intermediate
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free intro, Pro: 7 of 35 minutes free
Free: Three tasks on one menu tree · The brute force, and why it fails at this size · What to notice. With a pass: the other 11 sections and 3 interview answers, about 28 minutes.

What you will learn

  • Serialise and deserialise a binary tree with null markers, without recursion
  • Explain why markers and separators make the text unambiguous
  • Check whether one tree is a subtree of another by text search or by subtree IDs
  • Find duplicate subtrees with canonical strings, then in O(n) with ID triples
  • Choose between texts, IDs and hashes by their cost on deep trees

Before you start

What you’ll learn · Before you start

What you will learn

  • Serialise and deserialise a binary tree with null markers, without recursion
  • Explain why markers and separators make the text unambiguous
  • Check whether one tree is a subtree of another by text search or by subtree IDs
  • Find duplicate subtrees with canonical strings, then in O(n) with ID triples
  • Choose between texts, IDs and hashes by their cost on deep trees

Before you start

On this page

Two subtrees are duplicates when they have the same shape and the same values. To find them without comparing every pair of nodes, give each subtree a serial form, a text or a number that two subtrees share exactly when they are equal, and count the forms in a hash map. Preorder with a marker for every missing child is such a form, and it also saves a whole tree to text and loads it back. With the same forms, asking whether one tree occurs inside another becomes a search in a string or a lookup of one number.

Three tasks on one menu tree

An ordering app keeps its menu as a binary tree of whole-number codes, which may repeat and may be negative. Write three functions:

  • serialize(root) returns a string, and deserialize(text) turns it back into exactly the same tree.
  • is_subtree(menu, part): whether some node of menu heads a subtree identical to part, in shape and codes.
  • duplicate_subtrees(root): every kind of subtree that occurs at least twice, each kind reported once.

Limits: up to 10⁵ nodes in the menu and 10⁴ in the part, codes from −10⁹ to 10⁹, and trees that may be one long line.

The menu tree of twelve codes, 7 at the top; the two 3s, one under the 7 and one under the 8, head identical subtrees.783939555955RLRLRLRLRLL

The menu tree; the two subtrees headed by a double-bordered 3 are identical

Text description of the diagram

The diagram shows a binary tree of twelve nodes, each labelled with a code, drawn from the top. Each edge is marked L for a left child or R for a right child.

  • The top node is 7, with a left child 3 and a right child 8.
  • The 3 under the 7 has a left child 5 and a right child 9, and that 9 has a left child 5.
  • The 8 has a left child 3, with exactly the same subtree below it: a left child 5 and a right child 9 whose left child is 5.
  • The 8 also has a right child 9, whose only child is a 5 on the right.

The level-order list of this tree is [7, 3, 8, 5, 9, 3, 9, None, None, 5, None, 5, 9, None, 5, None, None, None, None, 5].

The two 3s have a double border because the subtrees they head are identical in shape and codes. Three kinds of subtree occur more than once: the single 5, which occurs five times; the 9 with a 5 on its left, twice; and the whole subtree headed by 3, twice. The 9 with a 5 on its right occurs only once, because its child hangs on the other side.

The part [3, 5, 9, None, None, 5] occurs twice in this menu and [9, 5, 5] not at all. Three kinds of subtree repeat: the single 5, the 9 with a 5 on its left, and the whole subtree under each 3.

The brute force, and why it fails at this size

For the subtree check, compare the part with the subtree at every node of the menu. For duplicates, write each subtree out as its own text and count the texts. Both are correct. The first does up to n × m node comparisons when codes repeat: 375,749 for a line of 1,000 equal codes and a part of 500 in the run below, and about 10⁹ at the limits. The second writes every node once for each of its ancestors: 2,428,495 characters for a line of 900, and tens of gigabytes for a line of 10⁵.

Comparing at every node, and writing every subtree out Python · brute/brute_force.py
"""Matching subtrees by brute force: compare at every node, and write out every subtree on its own; work counted."""
from collections import Counter, deque


class Node:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None


def build(values):
    """The tree of a level-order list, where None marks a missing child."""
    if not values or values[0] is None:
        return None
    root = Node(values[0])
    waiting = deque([root])
    i = 1
    while i < len(values):
        node = waiting.popleft()
        for side in ("left", "right"):
            if i < len(values) and values[i] is not None:
                child = Node(values[i])
                setattr(node, side, child)
                waiting.append(child)
            i += 1
    return root


def nodes_of(root):
    out, stack = [], [root] if root else []
    while stack:
        node = stack.pop()
        out.append(node)
        stack += [c for c in (node.right, node.left) if c]
    return out


compared = 0


def same(a, b):
    """Shape and codes compared node by node; stops at the first difference."""
    global compared
    pairs = [(a, b)]
    while pairs:
        x, y = pairs.pop()
        compared += 1
        if x is None or y is None:
            if x is not y:
                return False
            continue
        if x.val != y.val:
            return False
        pairs += [(x.right, y.right), (x.left, y.left)]
    return True


def is_subtree(menu, part):
    return any(same(node, part) for node in nodes_of(menu))


def written_out(node):
    """A subtree as text, built fresh for every subtree: preorder, '#' for a missing child."""
    if node is None:
        return "#"
    return f"{node.val},{written_out(node.left)},{written_out(node.right)}"


def duplicates(root):
    texts = [written_out(node) for node in nodes_of(root)]
    counts = Counter(texts)
    return sorted((t for t, c in counts.items() if c >= 2), key=len), sum(map(len, texts))


menu = build([7, 3, 8, 5, 9, 3, 9, None, None, 5, None, 5, 9, None, 5, None, None, None, None, 5])
for part in ([3, 5, 9, None, None, 5], [9, None, 5], [9, 5, 5]):
    compared = 0
    print(f"part {part} in the menu: {is_subtree(menu, build(part))}, {compared} comparisons")
kinds, chars = duplicates(menu)
print(f"subtrees that repeat: {kinds}, after writing {chars} characters")


def a_line(codes):
    """A line of first children with these codes, top first."""
    root = node = Node(codes[0])
    for code in codes[1:]:
        node.left = Node(code)
        node = node.left
    return root


compared = 0
print(f"equal codes, a line of 1,000 and a part of 500: {is_subtree(a_line([1] * 1000), a_line([1] * 499 + [2]))}, "
      f"{compared:,} comparisons")
print(f"a line of 900 codes: {duplicates(a_line(list(range(900))))[1]:,} characters written for 900 subtrees")

Output

part [3, 5, 9, None, None, 5] in the menu: True, 10 comparisons
part [9, None, 5] in the menu: True, 17 comparisons
part [9, 5, 5] in the menu: False, 21 comparisons
subtrees that repeat: ['5,#,#', '9,5,#,#,#', '3,5,#,#,9,5,#,#,#'], after writing 164 characters
equal codes, a line of 1,000 and a part of 500: False, 375,749 comparisons
a line of 900 codes: 2,428,495 characters written for 900 subtrees

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py

What to notice

Equal subtrees have equal texts, as long as the format records the shape as well as the codes. So both tasks reduce to producing one key per subtree cheaply and matching keys.

  • Preorder with null markers writes each code, and a marker such as # for each missing child.
  • String search looks for the part’s text inside the menu’s text, with separators so codes stay whole.
  • Canonical strings count each subtree’s text in a hash map.
  • ID triples give each distinct (left ID, code, right ID) a small number, so every key stays three numbers long.
  • Hashes of the children’s hashes give fixed-size keys, and two equal keys must still be checked.

A format that round-trips: preorder with null markers Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Why markers make the text unambiguous, with a proof
    • What the formats cost
  • Is one tree a subtree of another?
    • Subtree IDs: no text at all
  • Duplicate subtrees in O(n) with ID triples
    • Why equal IDs mean equal subtrees, with an invariant
    • Hashes of hashes
  • The code in five languages
  • Edge cases and common bugs
  • Variants
  • Follow-up questions
  • Exercise
    • Save, load and count the repeated parts of a menu tree
  • A practice set with sample answers
  • Check yourself
  • Interview questions
    • Why does a preorder serialisation of a binary tree need a marker for every missing child?
    • Check whether a template tree of 10⁴ nodes occurs as an exact subtree of a tree of 10⁶ nodes. Compare comparing at every node with matching serialised texts, including the worst case of each and the input that causes it.
    • One solution finds duplicate subtrees with a hash map keyed by each subtree's serialised text; another keys it by (left ID, value, right ID). Compare their time and memory on a chain of 10⁵ nodes and on a balanced tree, and say when the text version is good enough.
    • Design a text format to save and load binary trees whose values are arbitrary strings that may contain commas or newlines. What does your loader do with malformed, truncated or hostile input, such as a text that would build a chain of 10⁸ nodes?
  • Key takeaways

8 runnable examples (C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 23 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.