Serialize and Reconstruct
What is this?
Flattening a tree is easy. Flattening it so that exactly one tree can be read back is the problem. Every decision — whether to write null markers, whether to record how many children each node has, where the delimiters go — is a decision about ambiguity, and an encoding that loses information cannot be rescued by a clever parser.
The third problem here turns the same act sideways: if a subtree's serialisation is canonical, then two subtrees are identical exactly when their strings match, so the encoding becomes an equality test.
💡 Fun fact: an N-ary tree cannot be serialised as a bare preorder list. The sequence
1,2,3might be a root with two children, or a chain of three, and nothing in the string distinguishes them. Writing the child count after each value —1,2 2,0 3,0— makes the parse deterministic and single-pass. This is precisely why real formats carry length prefixes: protobuf, DNS messages and TLV encodings all solve the same ambiguity the same way.
🔓 The 3 problems in this chapter are free. Sign in with Google or Microsoft to start solving.
The one-line idea: choose an encoding that cannot be misread — null markers, brackets, or an explicit child count — and the parser becomes a single left-to-right consumption with no lookahead.
1. Parse with a cursor, not with slicing
Construct Binary Tree from String takes input like "4(2(3)(1))(6(5))": a value, then optionally a bracketed left subtree, then optionally a bracketed right. The natural implementation is a shared cursor consumed as you go:
i = 0
def parse():
global i
sign = 1
if s[i] == '-': sign = -1; i += 1
start = i
while i < len(s) and s[i].isdigit(): i += 1
node = TreeNode(sign * int(s[start:i]))
if i < len(s) and s[i] == '(':
i += 1; node.left = parse(); i += 1 # consume '(' … ')'
if i < len(s) and s[i] == '(':
i += 1; node.right = parse(); i += 1
return node
Two details matter. Negative values mean the digit scan must handle a leading -, which is easy to miss when the samples are all positive. And the cursor must be shared, not passed by value — slicing substrings instead is O(n²) and loses the position.
The rule that the first bracketed group is always the left child is what makes the grammar unambiguous: a node with only a right child is impossible to express, and the problem guarantees it never occurs.
2. Encode arity for N-ary trees
serialize: value, childCount, then each child recursively
deserialize: read value, read count, then read exactly that many children
The count tells the parser precisely when a node's children end, so no sentinel and no lookahead is needed. The alternative — a null marker after each child list — also works; what does not work is omitting both.
3. Serialisation as identity
For Find Duplicate Subtrees, serialise every subtree post-order and count the strings:
def walk(node):
if not node: return "#"
key = f"{node.val},{walk(node.left)},{walk(node.right)}"
count[key] += 1
if count[key] == 2: result.append(node) # append on the SECOND sighting only
return key
Appending on exactly the second occurrence is what stops a subtree appearing three times in the output when it occurs three times in the tree. And the # for null is essential — without it, a left-only and a right-only child serialise identically.
Worth stating: building a string per subtree makes this O(n²) in the worst case, because the strings' lengths sum quadratically on a skewed tree. Assigning each distinct subtree an integer id instead brings it back to O(n), and mentioning that trade unprompted is the senior answer.
4. A 30-second worked example (ambiguity)
N-ary preorder without arity: 1, 2, 3
could be: 1 or: 1
/ \ |
2 3 2
|
3
with child counts: 1,2 2,0 3,0 → only the first tree
1,1 2,1 3,0 → only the second
The bare list is one string describing two trees; the annotated list describes exactly one. That is the entire content of "choose an unambiguous encoding".
5. Where you'll actually meet this
- Wire formats. Length-prefixed encodings — protobuf, TLV, DNS — exist to solve this exact ambiguity.
- Compilers. Common-subexpression elimination hashes canonical subtree forms to find repeated computation.
- Caching and memoisation. Structural keys let identical subproblems share a cache entry.
- Version control and diffing. Detecting moved or duplicated subtrees between revisions.
- Document and config parsing. Bracketed nested formats are parsed with exactly this cursor discipline.
6. Problems in this chapter
▶ Construct Binary Tree from String
Rebuild a tree from "4(2(3)(1))(6(5))". A shared cursor, digit scanning with sign handling, and the rule that the first bracket is the left child.
Pattern: recursive descent with a shared cursor. Target: O(n) time, O(h) space.
▶ Serialize and Deserialize N-ary Tree
Round-trip a tree with arbitrary arity. Write each node's child count so the parser knows exactly when to stop.
Pattern: arity-encoded preorder. Target: O(n) both ways.
▶ Find Duplicate Subtrees
Every subtree occurring more than once. Serialise each subtree canonically and count; report on the second sighting.
Pattern: serialisation as a hash key. Target: O(n²) with strings, O(n) with subtree ids.
7. Common pitfalls 🚫
- Omitting null markers. A left-only child and a right-only child serialise identically without them.
- Serialising an N-ary tree without arity. The result cannot be parsed back at all.
- Slicing substrings while parsing instead of advancing a shared cursor —
O(n²)and it loses position. - Not handling negative values in the digit scan.
- Appending on every occurrence in the duplicate search, which emits a subtree once per repetition.
- Claiming
O(n)for the string-keyed version. It isO(n²)on a skewed tree; the id-based variant is the linear one. - Assuming the first bracket could be a right child. The grammar forbids it, and relying on that should be stated rather than assumed silently.
8. Key takeaways
- The encoding is the problem. Ambiguity cannot be parsed away.
- Explicit arity or explicit nulls — an N-ary tree needs one of them.
- Parse with a shared cursor, never by slicing.
- A canonical serialisation is an identity, which makes subtree comparison a map lookup.
- Report on the second sighting to emit each duplicate once.
- Know the cost of your keys. Strings are quadratic on skewed trees; integer ids are linear.
- Why interviewers like it: the traversal is trivial and the format decisions are everything, which makes it a direct test of how carefully you reason about representation.
Order: Construct Binary Tree from String → Serialize and Deserialize N-ary Tree → Find Duplicate Subtrees.
Core idea: Give every subtree a canonical fingerprint — a string built bottom-up as
value,left_fingerprint,right_fingerprint(with explicit markers for empty children). Two subtrees are duplicates iff their fingerprints match exactly. Count fingerprints in a hash map and grab a node the moment its fingerprint is seen for the second time.
Problem, rephrased
You're a build engineer staring at the expression trees an optimizing compiler produces. A single function compiles to one big tree; identical sub-expressions (say a*b + c computed in three different places) show up as structurally identical subtrees. Before emitting machine code you want to find every sub-expression that occurs more than once so you can compute it a single time and reuse the result. You don't need all the copies — just one representative of each repeated sub-expression.
Formally (LeetCode 652): given the root of a binary tree, return a list of roots — one per distinct subtree that appears two or more times. Two subtrees are the same when they have the same structure and the same node values. For each duplicated subtree you return any single one of its root nodes; order of the returned list doesn't matter.
| Input tree (level-order) | Duplicate subtrees (one root each) |
|---|---|
[1,2,3,4,null,2,4,null,null,4] |
subtree 4 (leaf) and subtree [2,4] |
[2,1,1] |
subtree 1 (leaf) |
[2,2,2,3,null,3,null] |
subtree 2→[3] and subtree 3 (leaf) |
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson