HARD Lab · Trees · 67

297. Serialize and Deserialize Binary Tree

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.

HardDFSPreorderTypeScript

PROBLEM What we're solving

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.

KEY IDEA Null markers make a traversal reversible

Insight → a bare preorder list of values is ambiguous— you can't tell where a subtree ends. Emit a sentinel (#) 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.

RECIPE Emit value + nulls, then replay the recursion

  • 1 · Serialize (preorder). For a real node, append its value, then recurse left, then right — value before children so the root is the first token.
  • 2 · Mark the holes. For a null node, append # and stop. These markers are what make the shape recoverable.
  • 3 · Deserialize (same order). Keep a single cursor 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.
  • 4 · Trust the cursor. Because each recursive call eats exactly one token before descending, the front-to-back stream lines up perfectly with the rebuild — no index math needed.
Classic confusion → you must emit # 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.

COST Complexity & alternatives

Search-on-decode (no markers)
O(n²)
Ambiguous; needs scans/heuristics to find subtree splits.
Preorder + null sentinels
O(n)
One pass each way; O(n) string, O(h) recursion stack.

Where the time goes

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).

Pattern transfer →the "traversal + null markers" trick also encodes BSTs (where you can even drop the markers and use value bounds), powers Construct Tree from Preorder & Inorder, and underlies Find Duplicate Subtrees(the serialization string is the subtree's identity).

RUN IT Preorder out, preorder back in

step 0 / 25
STARTSerialize. Walk the tree in preorder (node → left → right), appending each value to the output. A # marks a missing child so the shape is recoverable.
State
serialize
phase
emitting (serialize)consuming (deserialize)real node value# / null
slowfast

TYPESCRIPT The solution, annotated

serializeTree.ts
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();
}

Reading it block by block

The sentinel. 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.
serialize → preorder dfs. A 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.
Join into a string. out.join(',') turns the token list into the wire format. For the example tree you get 1,2,#,#,3,4,#,#,5,#,#.
deserialize → mirror recursion. Split back into tokens and keep one cursor i. build() consumes tokens[i++]: a # returns null; otherwise it makes a node and recursively builds left then right.
Why the cursor just works. 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.
Complexity → Both directions are O(n) time — each node and each null marker is touched once. Space is O(n) for the serialized string plus O(h) recursion stack (h = tree height, up to O(n) when skewed).

INTERVIEWFollow-ups they'll ask

  • "Do it iteratively / with BFS instead?" A level-order (queue) encoding also works — enqueue children, write # for nulls; decode with a queue and a cursor over the tokens.
  • "What if it's a BST?" You can skip null markers entirely: preorder values plus value-range bounds during decode are enough to place every node uniquely.
  • "Node values contain commas or the delimiter?" Use a length-prefixed or escaped encoding (e.g. len#value) so the parser can't be fooled.
  • "Make it more compact?" Drop trailing #markers, pack into a binary format, or use BFS so leaves don't each cost two sentinels.
  • "Reconstruct from two traversals?" Preorder + inorder (or postorder + inorder) also pins down a unique tree — the same recursion-with-a-cursor idea.

MNEMONIC The one-liner

"Preorder out with # for nulls; same recursion, one cursor, reads it back."

TRIGGERS When you see ___ → reach for ___

encode/decode a tree losslesslypreorder DFS + null sentinels
plain traversal is ambiguousemit # for every missing child
rebuild from a token streamshared cursor i, consume one per call
subtree identity / duplicate subtreesuse the serialization string as a key

SKELETON The reusable shape

skeleton.ts
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();
}

FLASHCARDS Tap to flip

Why must serialize emit a marker for null children?
Without #the value stream is ambiguous — you can't tell where a subtree ends, so the tree isn't recoverable.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
Why does serialize emit "#" for null children?
QUESTION 02
In a preorder serialization, the first token is always:
QUESTION 03
How does deserialize keep its place in the token stream?
QUESTION 04
serialize for the tree 1,2,3,null,null,4,5 (preorder, # for nulls) is:
QUESTION 05
Time complexity of serialize on an n-node tree?
QUESTION 06
If node values could themselves contain the comma delimiter, the fix is to:
QUESTION 07
For a Binary Search Tree specifically, you can: