Serialize and Deserialize Binary Tree
1. Problem & Core Objective
Design an algorithm to serialize a binary tree to a string and deserialize that string back into the identical tree.
String serialize(TreeNode root);
TreeNode deserialize(String data);There is no prescribed format — only that deserialize(serialize(tree)) reproduces the original.
1
/ \ → "1,2,#,#,3,4,#,#,5,#,#" → the same tree
2 3
/ \
4 5Constraints: 0 <= nodes <= 10^4 · -1000 <= Node.val <= 1000
What's actually being tested: that a traversal alone is ambiguous, and that adding null markers makes it unique. It's the same insight as question 6's serialization approach, now as the whole problem. The elegance is that deserialization consumes the string in exactly the order serialization produced it.
2. First-Principles Thought Process
Why a plain traversal isn't enough
Preorder of a tree with values 1, 2, 3 might be 1,2,3 — but that's consistent with several different trees:
A left-leaning chain and a right-leaning chain produce identical preorder output. The values are the same; only the shape differs, and plain preorder records no shape.
What's missing is where the structure stops
The ambiguity is about absent children. If the output recorded not just the nodes but also the nulls, the two trees would separate:
left-leaning: 1,2,3,#,#,#,#
right-leaning: 1,#,2,#,3,#,#Now they're distinguishable, and the format is unique for every tree. Null markers are the whole idea.
Why preorder in particular
Preorder emits the root first, which means deserialization can build top-down: read a token, create the node, then recursively build its left subtree and its right subtree from the tokens that follow.
Critically, the recursion consumes tokens in exactly the order serialization emitted them. A single moving pointer through the token list is all the state you need — the same trick as preIndex in question 13.
Inorder does not work: you'd have to know where the root is before reading it. Postorder and level-order both work, with adjustments.
Two format details that bite
Delimiters between values. Without a separator, 1 and 2 adjacent become 12. Comma-separate every token.
Negative numbers. Values can be negative, so a token may start with -. Splitting on commas handles this; anything that parses character-by-character does not.
3. Solution Paths
Approach 1 — Level-order with null markers
public String serialize(TreeNode root) {
if (root == null) return "";
StringBuilder sb = new StringBuilder();
Queue<TreeNode> q = new LinkedList<>(); // allows nulls
q.offer(root);
while (!q.isEmpty()) {
TreeNode n = q.poll();
if (n == null) { sb.append("#,"); continue; }
sb.append(n.val).append(',');
q.offer(n.left); // nulls enqueued deliberately
q.offer(n.right);
}
return sb.toString();
}
public TreeNode deserialize(String data) {
if (data.isEmpty()) return null;
String[] tokens = data.split(",");
TreeNode root = new TreeNode(Integer.parseInt(tokens[0]));
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < tokens.length) {
TreeNode parent = q.poll();
if (!tokens[i].equals("#")) {
parent.left = new TreeNode(Integer.parseInt(tokens[i]));
q.offer(parent.left);
}
i++;
if (i < tokens.length && !tokens[i].equals("#")) {
parent.right = new TreeNode(Integer.parseInt(tokens[i]));
q.offer(parent.right);
}
i++;
}
return root;
}- Time
O(n)both ways · SpaceO(w)queue,O(n)string
Counter-questions on this approach
⭐ "This is noticeably longer than the preorder version. Why?"
Because deserialization has to pair each dequeued parent with two consecutive tokens and manage the index by hand, with bounds checks on both. The preorder version's recursion consumes tokens implicitly, so there's no index arithmetic at all.
It's
O(n)either way; the preorder version is simply less code and fewer places to be off by one.
"Why LinkedList here rather than ArrayDeque?"
Because I deliberately enqueue
nullchildren as markers, andArrayDequethrows on null elements.LinkedListpermits them. That's a real constraint driving the choice, not a default — see 02.
"Is there any advantage to level-order?"
The output is human-readable and matches LeetCode's own input notation, which is convenient for debugging. And it's
O(w)rather thanO(h)in auxiliary space, so it's better on very deep trees. Otherwise preorder is cleaner.
Approach 2 — Preorder with null markers (optimal)
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
build(root, sb);
return sb.toString();
}
private void build(TreeNode n, StringBuilder sb) {
if (n == null) { sb.append("#,"); return; }
sb.append(n.val).append(',');
build(n.left, sb);
build(n.right, sb);
}
public TreeNode deserialize(String data) {
Deque<String> tokens = new ArrayDeque<>(Arrays.asList(data.split(",")));
return parse(tokens);
}
private TreeNode parse(Deque<String> tokens) {
String token = tokens.poll(); // consume exactly one
if (token == null || token.equals("#")) return null;
TreeNode node = new TreeNode(Integer.parseInt(token));
node.left = parse(tokens); // LEFT first — preorder demands it
node.right = parse(tokens);
return node;
}Trace — serializing then deserializing [1,2,3,null,null,4,5]:
Serialization emits 1,2,#,#,3,4,#,#,5,#,#,
parse call | Token consumed | Action |
|---|---|---|
| 1 | 1 | create 1; recurse left |
| 2 | 2 | create 2; recurse left |
| 3 | # | null — 2's left |
| 4 | # | null — 2's right; node 2 complete |
| 5 | 3 | create 3 (1's right); recurse left |
| 6 | 4 | create 4 |
| 7, 8 | #, # | 4's children; node 4 complete |
| 9 | 5 | create 5 (3's right) |
| 10, 11 | #, # | 5's children |
Tree reconstructed exactly ✓
- Time
O(n)both ways · SpaceO(n)string,O(h)stack
Counter-questions on this approach
⭐ "Why does a single shared token queue work? How does each call know which tokens are its own?"
Because preorder emits tokens in exactly the order the recursion consumes them: node, then the whole left subtree, then the whole right subtree. Each
parsecall takes exactly one token for itself, and its recursive calls consume precisely the tokens belonging to their subtrees.The subtree boundaries never need to be computed — they're implicit in the
#markers, which tell each branch when to stop descending.It works only if I recurse left before right, matching the serialization order. Swapping those two lines silently builds a mirrored tree.
⭐ "Why can't you use inorder?"
Because inorder emits the root between its subtrees, so on reading the first token you don't know whether it's the root or the leftmost descendant. You'd have to locate the root first, and there's nothing in the stream that marks it.
Postorder works if you consume the tokens backwards and build right before left — it's the mirror of preorder. Level-order works too, as above.
⭐ "What if a node's value could be the string #?"
Then the marker is ambiguous and the format breaks. Here values are integers bounded by 1000, so
#can never be a value — I verified that rather than assumed it. If values were arbitrary strings I'd escape them, or use a length-prefixed binary format where no sentinel is needed.Same discipline as the
−1sentinel in question 4 and thelongbounds in question 11: confirm the sentinel is outside the data's domain.
"Why StringBuilder and not string concatenation?"
Concatenation allocates a new string at every node, making serialization
O(n²).StringBuilderappends in amortizedO(1), keeping itO(n). Passing the builder down rather than returning strings is what makes that work.
"There's a trailing comma. Does that matter?"
String.split(",")discards trailing empty strings by default, so it's harmless. But it's sloppy — and if anyone switched tosplit(",", -1), which keeps them, an empty token would appear andparsewould treat it as neither#nor a number and throw. I'd either trim it or rely on thetoken == nullguard, which also covers a queue that runs dry.
"What about the empty tree?"
serialize(null)gives"#,", anddeserializereads#and returns null. It round-trips with no special case, which is worth checking rather than assuming.
"Stack depth at 10^4?"
A degenerate tree gives 10,000 frames in both directions. Survivable but close; the level-order version is
O(w)and is the safer choice for a very deep tree.
Comparison
| Approach | Serialize | Deserialize | Aux space | Notes |
|---|---|---|---|---|
| Level-order + markers | O(n) | O(n) | O(w) | Readable; more index arithmetic |
| Preorder + markers | O(n) | O(n) | O(h) | Least code; consumption order is implicit |
4. Why the Optimal Wins
Both are O(n) in each direction, so this is about code you can get right under pressure.
The preorder version's deserialization is six lines with no index arithmetic, because the recursion consumes tokens in precisely the order serialization produced them. The level-order version manages an explicit index, pairs each parent with two tokens, and needs bounds checks in two places — each an opportunity for an off-by-one.
The idea underneath both:
The framing worth keeping:
A traversal records values; null markers record structure. You need both, and with preorder the reconstruction falls out because the consumption order matches the emission order.
That's the same realisation as question 6, and it's why that question's serialization approach was worth understanding rather than dismissing.
5. Java Prerequisites
StringBuilder passed down
private void build(TreeNode n, StringBuilder sb) { ... } // not String build(...)Returning strings and concatenating makes it O(n²).
Splitting and consuming
Deque<String> tokens = new ArrayDeque<>(Arrays.asList(data.split(",")));
String t = tokens.poll(); // returns null when empty, rather than throwingString.split behaviour
"a,b,,".split(",") // ["a","b"] — trailing empties dropped
"a,b,,".split(",", -1) // ["a","b","",""] — keptLinkedList vs ArrayDeque — only LinkedList accepts null elements, which the level-order version needs.
Integer.parseInt handles a leading -, so negative values survive a comma split. Character-by-character parsing would not.
6. Interview Communication Guide
Clarifying questions: Is the format up to me (yes — only the round-trip matters)? Can values be negative (yes — it rules out single-character parsing)? Can the tree be empty (yes)? Is there a size limit on the string (matters if you're asked to compact it)? Must it be human-readable (affects preorder vs level-order)?
The pitch
"The core issue is that a traversal alone is ambiguous. A root with only a left child and a root with only a right child produce the same preorder sequence of values — the values match, only the shape differs, and plain preorder records no shape.
What's missing is where the structure stops. So I emit a marker for every null child. Then the two trees separate:
1,2,#,#,#versus1,#,2,#,#. Null markers are the whole idea.I use preorder because it emits the root first, which lets deserialization build top-down: read a token, create the node, recurse for the left subtree, recurse for the right.
The nice part is that the recursion consumes tokens in exactly the order serialization produced them — node, whole left subtree, whole right subtree. So a single moving pointer through the tokens is all the state I need; subtree boundaries never have to be computed, because the
#markers tell each branch when to stop. That works only if I recurse left before right, matching the emission order.Two format details. I comma-separate so adjacent values don't run together, and I use
#as the marker — which is safe here because values are integers bounded by 1000, so#can never be a value. I'd confirm that rather than assume it; if values were arbitrary strings I'd need escaping.
O(n)both directions,O(n)for the string andO(h)stack.Inorder wouldn't work, incidentally — it emits the root between its subtrees, so you can't tell from the first token whether it's the root or the leftmost node. Postorder works if you consume backwards and build right before left."
Edge cases to volunteer:
| Input | Tests |
|---|---|
null | Empty tree must round-trip |
[1] | Single node |
[1,2] vs [1,null,2] | The ambiguity the markers exist to resolve |
[-1,-2,-3] | Negative values — a leading - in a token |
[1000,-1000] | Multi-digit values; delimiters matter |
Degenerate, 10^4 nodes | Stack depth both ways |
| Perfect tree | Roughly half the tokens are # |
Name [1,2] vs [1,null,2]. It's the pair that motivates the entire design — and the negative-value case is what breaks anyone parsing one character at a time.
7. Follow-Up Questions — Modified Constraints
⭐ "Serialize a BST more compactly."
LeetCode 449. A BST needs no null markers at all — preorder alone determines it, because the BST property supplies the ordering constraint that markers would otherwise provide. Deserialize by tracking value bounds during the descent: a token belonging to the current subtree must fall within
(lo, hi), and the first token outside it terminates the subtree. That roughly halves the output, since a tree withnnodes hasn+1null slots.
⭐ "Make the encoding as small as possible."
Several directions. Drop the markers by pairing preorder with inorder (question 13) — no nulls, but two arrays. Use a succinct encoding:
2n+1bits of structure via balanced-parenthesis or LOUDS representation, plus the values packed separately, which approaches the information-theoretic minimum. Or just gzip the current format — the#runs compress extremely well.
"Serialize an n-ary tree."
Null markers are no longer sufficient, because arity is variable — a node with two children followed by a leaf is indistinguishable from one with three. Emit the child count after each value, or use an explicit end-of-children marker. This is the point where the binary shortcut stops generalising.
"What if the tree were a general graph with cycles?"
Traversal alone would loop forever. Assign each node an id, emit
(id, value, childIds)triples, and reconstruct by wiring ids in a second pass. That's what any real object serializer does, and it's why they all maintain an identity map.
"Deserialize without recursion."
Use an explicit stack, pushing nodes and tracking whether each still needs a left or right child. More code, but
O(h)on the heap — which is the answer if the tree could be10^6deep.
"What if serialize and deserialize ran on different machines?"
The format becomes a wire protocol, so byte order, character encoding, and versioning all matter. You'd reach for Protobuf, Avro, or similar rather than a hand-rolled comma format — and you'd want the schema versioned, since a format change has to be readable by older consumers.