On this page
Description
Serializing a tree turns it into a flat string you can store or send. Deserializing rebuilds the exact same tree. A list of values alone loses the tree's shape. So the encoding also writes a marker for every missing child. With those markers, one traversal is enough to rebuild the tree, as long as the encoder and the decoder use the same one.
We'll go from simple to more technical:
- The pattern: preorder with null markers, and one shared position in the string.
- A second flavor: level order with null markers and a queue.
- Where serialized trees live in production (syntax trees, query plans, caches).
- The bugs that rebuild a different tree without any error.
The approach
Write the tree in preorder: the node, then its left subtree, then its right subtree. Write # wherever a child is missing. To decode, read the tokens in the same order. Each call reads one token. A number makes a node, and the call then builds its left and right subtrees from the tokens that follow. A # means "no node here", and the call returns.
Take this tree:
Its preorder encoding is 5,2,#,#,9,7,#,#,11,#,#. The decoder reads it like this:
| Tokens | What the decoder does |
|---|---|
5 | makes the root, then builds its left subtree |
2 | makes 2 as the left child of 5 |
#, # | 2 has no children, so the left subtree of 5 is done |
9 | makes 9 as the right child of 5 |
7, #, # | makes 7 as the left child of 9, a leaf |
11, #, # | makes 11 as the right child of 9, a leaf |
Java
void serialize(TreeNode node, StringBuilder sb) {
if (node == null) { sb.append("#,"); return; }
sb.append(node.val).append(",");
serialize(node.left, sb);
serialize(node.right, sb);
}
TreeNode deserialize(Iterator<String> tokens) {
String val = tokens.next();
if (val.equals("#")) return null;
TreeNode node = new TreeNode(Integer.parseInt(val));
node.left = deserialize(tokens); // consumes exactly this subtree's share of tokens...
node.right = deserialize(tokens); // ...before this call reads its own
return node;
}
Python
def serialize(node: TreeNode | None, out: list[str]) -> None:
if node is None:
out.append("#")
return
out.append(str(node.val))
serialize(node.left, out)
serialize(node.right, out)
def deserialize(tokens) -> TreeNode | None: # tokens: ONE shared iterator, iter(data.split(","))
val = next(tokens)
if val == "#":
return None
node = TreeNode(int(val))
node.left = deserialize(tokens) # consumes exactly this subtree's share of tokens...
node.right = deserialize(tokens) # ...before this call reads its own
return node
Every call shares one iterator, so there's one position in the string. The left call reads exactly the tokens of the left subtree and stops. The next unread token is then the start of the right subtree.
A second flavor: level order with a queue
Level order writes the tree row by row, with null for each missing child. The tree above becomes 5,2,9,null,null,7,11,null,null,null,null. The encoder is a breadth-first walk that writes null instead of skipping missing children. The decoder runs the same walk. It takes a node from the queue and reads the next two tokens as that node's children:
Java
TreeNode deserialize(String data) {
String[] tokens = data.split(",");
if (data.isEmpty() || tokens[0].equals("null")) return null;
TreeNode root = new TreeNode(Integer.parseInt(tokens[0]));
Queue<TreeNode> queue = new ArrayDeque<>(List.of(root));
for (int i = 1; !queue.isEmpty(); i += 2) {
TreeNode node = queue.poll();
if (!tokens[i].equals("null")) { node.left = new TreeNode(Integer.parseInt(tokens[i])); queue.offer(node.left); }
if (!tokens[i + 1].equals("null")) { node.right = new TreeNode(Integer.parseInt(tokens[i + 1])); queue.offer(node.right); }
}
return root;
}
Python
from collections import deque
def deserialize(data: str) -> TreeNode | None:
tokens = data.split(",")
if not data or tokens[0] == "null":
return None
root = TreeNode(int(tokens[0]))
queue, i = deque([root]), 1
while queue:
node = queue.popleft()
node.left = None if tokens[i] == "null" else TreeNode(int(tokens[i]))
node.right = None if tokens[i + 1] == "null" else TreeNode(int(tokens[i + 1]))
queue.extend(child for child in (node.left, node.right) if child)
i += 2
return root
Both sides walk the tree in the same order, so the decoder always knows which node the next two tokens belong to.
Where you'll see it
Every tree that's saved to disk, sent over a network or cached goes through this, from a compiler's syntax tree to a database's cached query plan. The order is your choice, as long as the writer and the reader agree on it.
Which tool for which job?
| You need | Use |
|---|---|
| Any binary tree, simple code | preorder with null markers and a shared iterator |
| Level-by-level reading (the format most coding sites use) | level order with null markers |
| A BST | preorder without nulls - the bounds recover the shape |
| Input from outside (files, network) | an iterative decoder, plus validation |
| Format changes over time | a version marker at the start |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Skipping null children | different trees share one encoding | a marker for every null |
| Encoding in one order, decoding in another | a different tree, no error | one format, documented and tested both ways |
| A heap-style array (children at 2i+1, 2i+2) | exponential size for skewed trees | null markers only under real nodes |
| A recursive decoder on untrusted input | deep input overflows the stack | decode iteratively, limit depth |
| Splitting on a separator that can appear in values | values break apart | escape, or use a length prefix |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. An encoding writes only the values, with no null markers. Why can't the decoder rebuild the tree?
Show answer
Different trees give the same string. A root 5 with a left child 2, and a root 5 with a right child 2, both encode as 5,2. The markers record where children are missing, and that is the tree's shape. With them, the two trees become 5,2,#,#,# and 5,#,2,#,#.
2. A tree is written in level order, but the decoder reads the tokens as if they were preorder. What happens?
Show answer
You get a different tree, not an error. Neither side can check which order the other one used. The result looks like a perfectly valid tree, just not the one that was saved. A round-trip test catches it.
3. Why must the left and right calls in the preorder decoder share one iterator, instead of each getting its own?
Show answer
The shared iterator is the position in the string. When the left call returns, the next unread token is where the right subtree starts. With a fresh iterator per call, every call starts again from the root's token. It reads the root, recurses left, reads the root again, and never stops until the stack overflows.
Key takeaways
- Null markers record the tree's shape, so one traversal is enough to rebuild it.
- The encoder and decoder must use the same order. A mismatch builds a different tree with no error.
- Preorder with one shared iterator and level order with a queue are both fine choices.
- Test with a round trip:
deserialize(serialize(tree))must equal the original.