Skip to content

Volume ITechnology

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   5

To 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 (null pointers).

4.14.13 needed two traversals to rebuild a tree, because one alone is ambiguous:

  1        1
 /          \
2            2

Both 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)
   \
    2

One 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:

  1. parts = []: We use a list to collect string fragments. Appending to a list and joining at the end is O(n), whereas string concatenation s += str is O(n^2) in some languages.
  2. def walk(node):: A standard pre-order DFS.
  3. if not node: parts.append('#'); return: When we hit a null child, we explicitly record it as '#'.
  4. parts.append(str(node.val)): Record the node's value.
  5. walk(node.left), walk(node.right): Traverse left then right.
  6. return ','.join(parts): Stitch the parts together with commas.

Deserialization:

  1. 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 the next() token smoothly as it walks.
  2. def build():: Our recursive builder.
  3. token = next(values): Grab the very next token in the sequence.
  4. if token == '#': return None: If it's the null marker, return None (base case).
  5. node = TreeNode(int(token)): Otherwise, it's a number. Create the node.
  6. node.left = build(), node.right = build(): Recursively build the left child FIRST (because we serialized pre-order), then the right child.
  7. 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() grabs 1 \rightarrow makes node 1. Calls build() for left.
  • build() grabs 2 \rightarrow makes node 2. Calls build() for left.
  • build() grabs # \rightarrow returns None. 2.left = None. Calls build() for right.
  • build() grabs # \rightarrow returns None. 2.right = None.
  • Return to node 1. 1.left is finished. Calls build() for 1.right.
  • build() grabs 3 \rightarrow makes node 3. Calls build() for left.
  • build() grabs 4 \rightarrow makes node 4. Left #, right #. Returns 4. 3.left = 4.
  • build() grabs 5 \rightarrow makes node 5. Left #, right #. Returns 5. 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 parts of 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).

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 pickle all 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.