Appearance
4.14.15 — Serialize and Deserialize Binary Tree
LeetCode 297 · Hard · ★ Blind 75
The problem
Write a tree to a string, and rebuild the identical tree from that string. The format is yours to choose.
1
/ \
2 3 → some string → the same tree back
/ \
4 5
4 5To put this in plain English: you need to convert a 3D tree structure into a flat 1D string, and then write a function that can read that 1D string and perfectly reconstruct the 3D tree.
Constraints to Keep in Mind:
0 <= Number of nodes in the tree <= 10^4-1000 <= Node.val <= 1000- The tree can be empty, unbalanced, or huge.
The Naive Approach
From a previous problem (Construct Binary Tree from Preorder and Inorder), we know that we can uniquely identify a tree if we have BOTH its preorder and inorder traversals.
- How it works: To serialize, you could run a preorder traversal and an inorder traversal, joining them both into a string like
"pre:1,2,3|in:2,1,3". To deserialize, you'd parse out the two arrays and run the O(n) build function. - Why we can do better: While this works, it requires doing two full traversals, storing twice as much data in the string, and running a somewhat complex reconstruction algorithm. We can serialize a tree using only ONE traversal if we simply record the empty spaces (
nullpointers).
4.14.13 needed two traversals to rebuild a tree, because one alone is ambiguous:
1 1
/ \
2 2Both give pre-order [1, 2].
But the ambiguity only exists because you cannot tell a missing left child from a missing right child. Write the missing children down and it disappears.
1 → 1, 2, #, #, # (1, then left=2 with two nulls, then right=null)
/
2
1 → 1, #, 2, #, # (1, then left=null, then right=2)
\
2One traversal is now enough. That is the whole idea, and it is the same insight as 4.4.6 Encode and Decode Strings: make the encoding self-describing and the decoder never has to guess.
Why pre-order
Pre-order writes the root first. So the decoder reads a value, creates that node, and then recursively builds its left subtree and its right subtree — consuming the string in exactly the order it was written. The reader needs no lookahead and no index arithmetic.
In-order would not work at all, because the decoder would not know which value is the root of the piece it is reading.
The solution
python
class Codec:
def serialize(self, root) -> str:
parts = []
def walk(node):
if not node:
parts.append('#')
return
parts.append(str(node.val))
walk(node.left)
walk(node.right)
walk(root)
return ','.join(parts)
def deserialize(self, data: str):
values = iter(data.split(','))
def build():
token = next(values)
if token == '#':
return None
node = TreeNode(int(token))
node.left = build() # order matters: left, then right
node.right = build()
return node
return build()ts
function serialize(root: TreeNode | null): string {
const parts: string[] = [];
(function walk(node: TreeNode | null) {
if (!node) { parts.push('#'); return; }
parts.push(String(node.val));
walk(node.left);
walk(node.right);
})(root);
return parts.join(',');
}
function deserialize(data: string): TreeNode | null {
const values = data.split(',');
let i = 0;
function build(): TreeNode | null {
const token = values[i++];
if (token === '#') return null;
const node = new TreeNode(Number(token));
node.left = build();
node.right = build();
return node;
}
return build();
}Code Walkthrough
Serialization:
parts = []: We use a list to collect string fragments. Appending to a list and joining at the end is O(n), whereas string concatenations += stris O(n^2) in some languages.def walk(node):: A standard pre-order DFS.if not node: parts.append('#'); return: When we hit a null child, we explicitly record it as'#'.parts.append(str(node.val)): Record the node's value.walk(node.left),walk(node.right): Traverse left then right.return ','.join(parts): Stitch the parts together with commas.
Deserialization:
values = iter(data.split(',')): We split the string back into a list of tokens and turn it into an iterator. The iterator allows our recursive function to pull thenext()token smoothly as it walks.def build():: Our recursive builder.token = next(values): Grab the very next token in the sequence.if token == '#': return None: If it's the null marker, returnNone(base case).node = TreeNode(int(token)): Otherwise, it's a number. Create the node.node.left = build(),node.right = build(): Recursively build the left child FIRST (because we serialized pre-order), then the right child.return node: Return the fully constructed subtree.
Every node writes exactly three things: its value, its left subtree, its right subtree. A null writes one thing. That regularity is what makes the decoder a mirror image of the encoder.
The iterator (or the index) must be shared across all recursive calls, because each call consumes tokens that the next one must not re-read. Python's iter gives this naturally; TypeScript uses a closure variable.
The comma matters. Joining with nothing would make 1 and 2 indistinguishable from 12. The same delimiter reasoning as 4.4.6 — except here the values are numbers and cannot contain a comma, so a plain separator is safe. If values could be arbitrary strings, you would need length prefixing instead.
Build left before right, matching the order they were written.
Hand Trace
Serialising the example tree:
1, 2, #, #, 3, 4, #, #, 5, #, #Reading it back:
build()grabs1\rightarrow makes node1. Callsbuild()forleft.build()grabs2\rightarrow makes node2. Callsbuild()forleft.build()grabs#\rightarrow returnsNone.2.left = None. Callsbuild()forright.build()grabs#\rightarrow returnsNone.2.right = None.- Return to node 1.
1.leftis finished. Callsbuild()for1.right. build()grabs3\rightarrow makes node3. Callsbuild()forleft.build()grabs4\rightarrow makes node4. Left#, right#. Returns4.3.left = 4.build()grabs5\rightarrow makes node5. Left#, right#. Returns5.3.right = 5.- Done, and the input is exactly consumed.
Complexity
- Time Complexity: O(n) for both serialization and deserialization. We visit each of the n nodes exactly once. For serialization, we also visit n+1 null pointers. The
.join()and.split()operations take time proportional to the length of the string, which is O(n). Thus, time is strictly linear. - Space Complexity: O(n).
- Serialization uses an array
partsof size 2n + 1 (which is O(n)). The call stack takes O(h) space. - Deserialization splits the string into an array of 2n + 1 tokens (O(n)) and also uses O(h) space on the call stack. Overall space is dominated by O(n).
- Serialization uses an array
The BFS alternative
You can serialise level by level instead, which is what LeetCode's own display format does:
[1, 2, 3, null, null, 4, 5]Deserialising uses a queue: take a node off, read the next two tokens as its children, and push any non-null children back on.
It is equally valid and produces a more human-readable string. Pre-order is usually easier to write under pressure because the encoder and decoder are mirror images, while the BFS version needs a queue on both sides.
Where this goes next
- Serialize and Deserialize BST (LeetCode 449) — a search tree needs no null markers at all, because the ordering already tells the decoder where each subtree ends. That produces a shorter string, and noticing why is a good test of whether you understand what the markers are for.
- Find Duplicate Subtrees — serialise every subtree and count identical strings in a hash map.
- Real systems — JSON, Protocol Buffers, and Python's
pickleall face this problem, and all of them solve it with self-description: either explicit markers or length prefixes. Chapter 10.4 covers serialisation formats properly.
The rule: a traversal plus explicit null markers determines a tree uniquely. Self-describing data needs no second traversal.
What the interviewer will push on
"Why is one traversal enough here when 4.14.13 needed two?" The null markers remove the ambiguity.
"Why pre-order and not in-order?" The decoder must know the root before it can build the subtrees.
"Why the separator?" Multi-digit values would run together.
"What if node values were arbitrary strings?" Delimiters stop being safe, so length-prefix each value, exactly as in 4.4.6.
"How much bigger is the string than the tree?" 2n + 1 tokens, since a tree with n nodes has n+1 empty child slots.
"Could you do it without null markers?" For a BST, yes — the ordering substitutes for them.
One thing to volunteer: say why the naive traversal is ambiguous, with the two-node example, before you propose the fix. The fix only sounds clever when the problem it solves has been named.
Next: 4.15 is short and specific — three problems built on the trie from Chapter 4.13.4, and one of them is where backtracking and tries meet.