Encode any binary tree to a string and decode it back to the exact same tree. A preorder DFS that emits a sentinel # for every null child makes the shape self-describing — so reading the tokens back front-to-back rebuilds it in one pass.
Design serialize(root) (tree → string) and deserialize(data) (string → tree) so that deserialize(serialize(t)) reproduces t exactly. For the tree 1,2,3,null,null,4,5 (root 1; left 2; right 3 with children 4 and 5), a preorder encoding is 1,2,#,#,3,4,#,#,5,#,#, which must decode back to the same tree.
#) for every missing child and the stream becomes a complete, self-describing blueprint. Decoding is then a mirror of encoding: the same recursion consumes one token per call, in the same order, so the recursion stack reconstructs identical structure.null node, append # and stop. These markers are what make the shape recoverable.i. Read one token: if #, this child is null; else create the node, then build its left subtree, then its right — consuming tokens in the exact order serialize produced them.# for nulls. Without them, 1,2 could be "2 is the left child" or"2 is the right child" — undecidable. A preorder traversal that records nulls is reversible; one that skips them is not.Serialize visits each of n real nodes plus the n+1 null slots exactly once → O(n). Deserialize consumes each token once → O(n). Space is O(n) for the string and O(h) for the recursion stack (h = height; O(n) worst case for a skewed tree).
preorder(node → left → right), appending each value to the output. A # marks a missing child so the shape is recoverable.1class TreeNode {2 val: number;3 left: TreeNode | null;4 right: TreeNode | null;5 constructor(val: number) {6 this.val = val;7 this.left = null;8 this.right = null;9 }10}1112const NULL = '#';1314// Preorder DFS: node, then left, then right. '#' marks an absent child.15▶function serialize(root: TreeNode | null): string {16▶ const out: string[] = [];17▶ const dfs = (node: TreeNode | null): void => {18 if (node === null) {19 out.push(NULL); // record the hole so shape survives20 return;21 }22 out.push(String(node.val)); // emit value BEFORE descending (preorder)23 dfs(node.left);24 dfs(node.right);25 };26 dfs(root);27 return out.join(',');28}2930// Rebuild by consuming the same preorder stream front-to-back.31function deserialize(data: string): TreeNode | null {32 const tokens = data.split(',');33 let i = 0;34 const build = (): TreeNode | null => {35 const token = tokens[i++]; // consume one token, advance36 if (token === NULL) return null;37 const node = new TreeNode(Number(token));38 node.left = build(); // left subtree consumes next tokens39 node.right = build(); // right subtree consumes what remains40 return node;41 };42 return build();43}
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val: number) {
this.val = val;
this.left = null;
this.right = null;
}
}
const NULL = '#';
// Preorder DFS: node, then left, then right. '#' marks an absent child.
function serialize(root: TreeNode | null): string {
const out: string[] = [];
const dfs = (node: TreeNode | null): void => {
if (node === null) {
out.push(NULL); // record the hole so shape survives
return;
}
out.push(String(node.val)); // emit value BEFORE descending (preorder)
dfs(node.left);
dfs(node.right);
};
dfs(root);
return out.join(',');
}
// Rebuild by consuming the same preorder stream front-to-back.
function deserialize(data: string): TreeNode | null {
const tokens = data.split(',');
let i = 0;
const build = (): TreeNode | null => {
const token = tokens[i++]; // consume one token, advance
if (token === NULL) return null;
const node = new TreeNode(Number(token));
node.left = build(); // left subtree consumes next tokens
node.right = build(); // right subtree consumes what remains
return node;
};
return build();
}NULL = '#'is the marker for an absent child. Picking a value that can't appear as a real node (here a non-numeric character) keeps decoding unambiguous.null node pushes # and returns. A real node pushes its value first, then recurses left and right. Emitting before descending is what makes the very first token the root.out.join(',') turns the token list into the wire format. For the example tree you get 1,2,#,#,3,4,#,#,5,#,#.i. build() consumes tokens[i++]: a # returns null; otherwise it makes a node and recursively builds left then right.build reads exactly one token before recursing, in the same order serialize wrote them. So the shared, advancing i walks the stream front-to-back and the call stack rebuilds the identical shape.# for nulls; decode with a queue and a cursor over the tokens.len#value) so the parser can't be fooled.#markers, pack into a binary format, or use BFS so leaves don't each cost two sentinels.class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val: number) {
this.val = val;
this.left = null;
this.right = null;
}
}
const NULL = '#';
// Preorder DFS: node, then left, then right. '#' marks an absent child.
function serialize(root: TreeNode | null): string {
const out: string[] = [];
const dfs = (node: TreeNode | null): void => {
if (node === null) {
out.push(NULL); // record the hole so shape survives
return;
}
out.push(String(node.val)); // emit value BEFORE descending (preorder)
dfs(node.left);
dfs(node.right);
};
dfs(root);
return out.join(',');
}
// Rebuild by consuming the same preorder stream front-to-back.
function deserialize(data: string): TreeNode | null {
const tokens = data.split(',');
let i = 0;
const build = (): TreeNode | null => {
const token = tokens[i++]; // consume one token, advance
if (token === NULL) return null;
const node = new TreeNode(Number(token));
node.left = build(); // left subtree consumes next tokens
node.right = build(); // right subtree consumes what remains
return node;
};
return build();
}Encode the tree breadth-first with # for nulls — this is the familiar LeetCode array format, and a single queue wires children back up on decode.
const NULL = '#';
// Level-order: sweep the tree with a queue, writing every value and a
// '#' for each missing child, row by row.
function serialize(root: TreeNode | null): string {
if (root === null) return NULL;
const out: string[] = [];
const queue: (TreeNode | null)[] = [root];
while (queue.length > 0) {
const node = queue.shift() as TreeNode | null;
if (node === null) {
out.push(NULL); // record the hole; it has no children to enqueue
continue;
}
out.push(String(node.val)); // emit value, then queue BOTH children (even nulls)
queue.push(node.left);
queue.push(node.right);
}
return out.join(',');
}
// Decode with a cursor over the tokens: the first token is the root, then
// each real node pulls its next two tokens as left/right children.
function deserialize(data: string): TreeNode | null {
const tokens = data.split(',');
if (tokens[0] === NULL) return null;
const root = new TreeNode(Number(tokens[0]));
const queue: TreeNode[] = [root];
let i = 1; // tokens[0] already consumed as root
while (queue.length > 0) {
const node = queue.shift() as TreeNode;
const leftTok = tokens[i++]; // next token is this node's left child
if (leftTok !== NULL) {
node.left = new TreeNode(Number(leftTok));
queue.push(node.left);
}
const rightTok = tokens[i++]; // then its right child
if (rightTok !== NULL) {
node.right = new TreeNode(Number(rightTok));
queue.push(node.right);
}
}
return root;
}Number(tok) handles negatives for free, and the queue guarantees parent/child order matches the write order — so unbalanced trees round-trip exactly.Serialize as left, right, root — so the root lands last. Decoding then consumes the stream from the back, building the right child before the left.
const NULL = '#';
// Postorder: left subtree, then right subtree, then the node itself.
// The very last token is therefore the overall root.
function serialize(root: TreeNode | null): string {
const out: string[] = [];
const dfs = (node: TreeNode | null): void => {
if (node === null) {
out.push(NULL); // mark the hole so shape survives
return;
}
dfs(node.left);
dfs(node.right);
out.push(String(node.val)); // emit value AFTER both children (postorder)
};
dfs(root);
return out.join(',');
}
// Mirror the postorder by consuming from the RIGHT end. Each node pops its
// own token first, then builds RIGHT before LEFT (reverse of write order).
function deserialize(data: string): TreeNode | null {
const tokens = data.split(',');
let i = tokens.length - 1; // cursor walks backwards
const build = (): TreeNode | null => {
const token = tokens[i--]; // consume one token from the tail
if (token === NULL) return null;
const node = new TreeNode(Number(token));
node.right = build(); // right was written last (before root) → read first
node.left = build(); // left was written before right → read second
return node;
};
return build();
}| encode/decode a tree losslessly | preorder DFS + null sentinels |
| plain traversal is ambiguous | emit # for every missing child |
| rebuild from a token stream | shared cursor i, consume one per call |
| subtree identity / duplicate subtrees | use the serialization string as a key |
const NULL = '#';
function serialize(root: TreeNode | null): string {
const out: string[] = [];
const dfs = (n: TreeNode | null) => {
if (!n) { out.push(NULL); return; }
out.push(String(n.val));
dfs(n.left); dfs(n.right);
};
dfs(root);
return out.join(',');
}
function deserialize(data: string): TreeNode | null {
const t = data.split(','); let i = 0;
const build = (): TreeNode | null => {
const x = t[i++];
if (x === NULL) return null;
const node = new TreeNode(Number(x));
node.left = build(); node.right = build();
return node;
};
return build();
}#the value stream is ambiguous — you can't tell where a subtree ends, so the tree isn't recoverable.