The catalog-pricing console from Level Order now has to snapshot the split tree, ship it across a restart, and rebuild the same questions. The intern reused that walk: values grouped by floor, missing children omitted. A full tree round-tripped. A left-only child came back sitting on the right — the string had lost the holes. A second intern dumped a heap-sized slot array; a spine filled millions of cells before the snapshot finished.
Serialize and deserialize must round-trip: same shape, same values. The binary tree post owns layout. The BFS post owns the FIFO ArrayDeque. Level Order is the same floor walk that drops nulls so a dashboard has no holes. Here we keep them: a "null" token in the string, never a null offered to the deque.
The problem
Design a codec: serialize turns a binary tree into a string; deserialize rebuilds a tree from that string. The pair is identity on structure and values. An empty tree is "null". A single node is that value plus two "null" children.
final class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
Sample trees. The string records every missing child of a live node. Level Order would return [[8], [3], [5]] for the second drawing and forget which side each child sat on.
8
/ \
3 10
/ \
9 14
→ 8,3,10,null,null,9,14,null,null,null,null
8
/
3
\
5
→ 8,3,null,null,5,null,null
(root = null) → null
[1] → 1,null,null
Note: Empty is the token "null", not an empty string. Use the same token for every hole. Comma-separated ints plus "null" is enough; this is not JSON.
A full heap-index array is the honest brute force
Allocate 2^(h+1)-1 slots, write a node at i, left at 2*i+1, right at 2*i+2, fill the rest with "null". Deserialize reads the same indices. Correct. A complete tree is dense. A spine of height h still pays Θ(2^h) cells.
String serializeHeap(TreeNode root) {
if (root == null) {
return "null";
}
int cap = (1 << (height(root) + 1)) - 1;
String[] slots = new String[cap];
Arrays.fill(slots, "null");
fill(root, 0, slots);
return String.join(",", slots);
}
void fill(TreeNode node, int i, String[] slots) {
if (node == null) {
return;
}
slots[i] = String.valueOf(node.val);
fill(node.left, 2 * i + 1, slots);
fill(node.right, 2 * i + 2, slots);
}
int height(TreeNode node) {
return node == null ? -1 : 1 + Math.max(height(node.left), height(node.right));
}
At a dozen balanced nodes this is a rounding error. At a left spine of thirty questions you allocated a billion-slot string (1 << 30): encode the holes in the string, not a cell per cousin in a heap layout.
BFS, write null tokens, never offer null
Offer the root. While the queue is not empty, poll a live node, then for left and for right: if the child exists, append its value and offer it; if not, append "null" and do not offer. Children of a null are never considered — we never enqueued that null.
ArrayDeque refuses null. The holes live in the string, not on the deque.
q: [8] out: 8
poll 8 left 3 → offer 3 right null → token
q: [3] out: 8,3,null
poll 3 left null → token right 5 → offer 5
q: [5] out: 8,3,null,null,5
poll 5 left null right null
q: [] out: 8,3,null,null,5,null,null
Deserialize: if the string is "null", return null. Split on commas. The first token is the root; offer it. Then for each polled live node, consume the next two tokens as left and right: a "null" token leaves that child empty (do not offer); otherwise build the child, attach it, offer it.
The Java is that walk. Use Deque and ArrayDeque, not java.util.Stack. Never offer null — holes are "null" tokens in the string.
String serialize(TreeNode root) {
if (root == null) {
return "null";
}
List<String> out = new ArrayList<>();
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
out.add(String.valueOf(root.val));
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node.left != null) {
out.add(String.valueOf(node.left.val));
q.offer(node.left);
} else {
out.add("null");
}
if (node.right != null) {
out.add(String.valueOf(node.right.val));
q.offer(node.right);
} else {
out.add("null");
}
}
return String.join(",", out);
}
TreeNode deserialize(String data) {
if (data.equals("null")) {
return null;
}
String[] tokens = data.split(",");
TreeNode root = new TreeNode(Integer.parseInt(tokens[0]));
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < tokens.length) {
TreeNode node = q.poll();
if (!tokens[i].equals("null")) {
node.left = new TreeNode(Integer.parseInt(tokens[i]));
q.offer(node.left);
}
i++;
if (i < tokens.length && !tokens[i].equals("null")) {
node.right = new TreeNode(Integer.parseInt(tokens[i]));
q.offer(node.right);
}
i++;
}
return root;
}
Time is O(n) — each live node is offered once, polled once, and writes two child tokens. Space is O(w) for the queue, w being the widest floor, plus O(n) for the string. A spine stays a thin queue and a linear string. Do not quote a graph seen set; there is nothing to mark.
Note: Return "null" for an empty root before you offer. Offering null throws on ArrayDeque. Increment i after a "null" token too — that slot is consumed even though nothing was attached.
What interviewers usually poke next
- Preorder with nulls.
val + "," + serialize(left) + "," + serialize(right), and"null"when the node is missing. A second codec, same round-trip, DFS frames instead of a queue. Same Tree already uses that dump as a brute compare. - Not JSON. Nested objects or a JSON array of nodes is a third encoding. The prompt is a string codec; comma tokens plus
"null"already round-trip. - Level Order. Same BFS, drops nulls, groups by floor. Different contract. Do not paste
[[8], [3], [5]]into this string.
You are done with this problem when you can walk 8,3,null,null,5,null,null out loud, rebuild the left-then-right-child tree, and say why the level-order lists cannot be the codec.