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.
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, anddeserialize(text)turns it back into exactly the same tree.is_subtree(menu, part): whether some node ofmenuheads a subtree identical topart, 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; 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⁵.
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
- NIST Dictionary of Algorithms and Data Structures: preorder traversal (National Institute of Standards and Technology (NIST))
- What's New in Python 3.10: optimizations (substring search) (Python Software Foundation)
- dict.setdefault() (The Python Standard Library) (Python Software Foundation)
- str.split() (The Python Standard Library) (Python Software Foundation)
- The Java Language Specification, Java SE 25: 8.10 Record Classes (Oracle)
- Java SE 25 API: java.util.IdentityHashMap (Oracle)
- The Go Programming Language Specification: comparison operators (The Go Authors)
- std::unordered_map (cppreference.com) (cppreference.com)
- std::hash (cppreference.com) (cppreference.com)
- ECMAScript 2026 Language Specification: Map objects (Ecma International)
- Fast Pattern Matching in Strings (D. E. Knuth, J. H. Morris and V. R. Pratt, SIAM Journal on Computing 6(2)) (Society for Industrial and Applied Mathematics)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress