Serialize and Deserialize Binary Tree
Write the tree in pre-order with a marker for every empty spot, then read it back in the same order. Solved with one pre-order pass each way in O(n) time and O(n) space, with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”Design two functions for a binary tree.
serialize(root)turns the tree into a single string.deserialize(data)takes that string and rebuilds the exact same tree: the same values in the same places.
You choose the string format. The only rule is that deserialize(serialize(root)) must give back a tree
equal to root. The functions must not keep any state between calls.
Trees in the examples are written level by level, from left to right. null marks a missing child.
Example 1
Input: root = [6, 2, 9, null, null, 7]
Output: [6, 2, 9, null, null, 7]
Any format works if it round-trips. The one on this page writes 6,2,#,#,9,7,#,#,#.
Example 2
Input: root = []
Output: []
The empty tree must survive the round trip too. Our format writes a single #.
Example 3
Input: root = [-5, null, 4]
Output: [-5, null, 4]
Negative values and a missing left child must both come back as they were.
Constraints
Section titled “Constraints”- The tree has n nodes, where 0 ≤ n ≤ 10,000.
- -1,000 ≤ Node.val ≤ 1,000
Clarifying questions
Section titled “Clarifying questions”- Is the string format fixed?
- No. Any format is fine as long as deserialize rebuilds the same tree.
- Can values be negative or have several digits?
- Yes. So values need a separator, such as a comma, or "12" could be read as 1 and 2.
- Can the tree be empty?
- Yes. The string must still record that, so deserialize can return None.
- Can we store data between the two calls?
- No. Everything needed to rebuild the tree must be in the string.
Spot the pattern
Section titled “Spot the pattern”A list of values alone loses the shape: many trees share the same pre-order values. But if every empty spot is written down too, the shape is fixed. Reading the list back in the same order rebuilds it, one node per value. Both directions are a tree traversal: the same pre-order recursion, once to write and once to read.
Solutions
Section titled “Solutions”There is only one approach worth showing. Every node must be written and read once, and this does exactly that in each direction. A level-order format works too, at the same cost.
Pre-order with markers O(n)
The idea
Section titled “The idea”Serialize. Walk the tree in pre-order: write the node’s value, then its left subtree, then its right
subtree. When a child is missing, write #. Join everything with commas.
Deserialize. Split the string at the commas and read the tokens in the same order. A # means
“empty here”, so return None. A number means “a node here”: make it, then read its left subtree, then its
right subtree, in the same way.
The reader never has to look ahead or count. The # marks tell it exactly where each subtree ends,
because every subtree is written as its root followed by two complete subtrees.
Why it is correct: the reader makes the same calls in the same order as the writer. Each token is read by the call that matches the call that wrote it, so every node ends up in its old place.
Walkthrough
Section titled “Walkthrough”- def serialize(node, parts):
- if node is None:
- parts.append("#"); return
- parts.append(str(node.val))
- serialize(node.left, parts)
- serialize(node.right, parts)
- def deserialize(tokens):
- token = next(tokens)
- if token == "#": return None
- node = TreeNode(int(token))
- node.left = deserialize(tokens)
- node.right = deserialize(tokens)
- return node
Serialize: walk the tree in pre-order (node, left subtree, right subtree) and write each value. Write # for every empty spot, so the shape is kept.
// Pre-order with a marker for every empty spot. The markers fix the// shape, so one list is enough to rebuild the tree. n steps each way.class Codec { public String serialize(TreeNode root) { List<String> parts = new ArrayList<>(); walk(root, parts); return String.join(",", parts); }
private void walk(TreeNode node, List<String> parts) { // "#" records an empty spot, so the shape is kept. if (node == null) { parts.add("#"); return; } // Node first, then its subtrees: the reader meets the // root of each part before the part itself. parts.add(String.valueOf(node.val)); walk(node.left, parts); walk(node.right, parts); }
private String[] tokens; private int position;
public TreeNode deserialize(String data) { tokens = data.split(","); position = 0; return read(); }
private TreeNode read() { // Read in the same order as the writer wrote. String token = tokens[position++]; if (token.equals("#")) return null; TreeNode node = new TreeNode(Integer.parseInt(token)); // The left subtree was written first, so read it first. // Its own "#" marks tell us where it ends. node.left = read(); node.right = read(); return node; }}import sys
# A tree can be as deep as it has nodes (a chain), and Python stops at 1,000 nested calls by default.# Raise the limit to cover the deepest tree the constraints allow.sys.setrecursionlimit(20_000)
# Pre-order with a marker for every empty spot. The markers fix the# shape, so one list is enough to rebuild the tree. n steps each way.class Codec: def serialize(self, root: TreeNode | None) -> str: parts = []
def walk(node): # "#" records an empty spot, so the shape is kept. if node is None: parts.append("#") return # Node first, then its subtrees: the reader meets the # root of each part before the part itself. parts.append(str(node.val)) walk(node.left) walk(node.right)
walk(root) return ",".join(parts)
def deserialize(self, data: str) -> TreeNode | None: tokens = iter(data.split(","))
def read(): # Read in the same order as the writer wrote. token = next(tokens) if token == "#": return None node = TreeNode(int(token)) # The left subtree was written first, so read it first. # Its own "#" marks tell us where it ends. node.left = read() node.right = read() return node
return read()// Pre-order with a marker for every empty spot. The markers fix the// shape, so one list is enough to rebuild the tree. n steps each way.public class Codec{ public string serialize(TreeNode root) { var outs = new List<string>(); Walk(root, outs); return string.Join(",", outs); }
private void Walk(TreeNode node, List<string> outs) { // "#" records an empty spot, so the shape is kept. if (node == null) { outs.Add("#"); return; } // Node first, then its subtrees: the reader meets the // root of each part before the part itself. outs.Add(node.val.ToString()); Walk(node.left, outs); Walk(node.right, outs); }
private string[] tokens; private int position;
public TreeNode deserialize(string data) { tokens = data.Split(','); position = 0; return Read(); }
private TreeNode Read() { // Read in the same order as the writer wrote. string token = tokens[position++]; if (token == "#") return null; var node = new TreeNode(int.Parse(token)); // The left subtree was written first, so read it first. // Its own "#" marks tell us where it ends. node.left = Read(); node.right = Read(); return node; }}// Pre-order with a marker for every empty spot. The markers fix the// shape, so one list is enough to rebuild the tree. n steps each way.type Codec struct{}
func Constructor() Codec { return Codec{} }
func (codec *Codec) serialize(root *TreeNode) string { parts := []string{} var walk func(node *TreeNode) walk = func(node *TreeNode) { // "#" records an empty spot, so the shape is kept. if node == nil { parts = append(parts, "#") return } // Node first, then its subtrees: the reader meets the // root of each part before the part itself. parts = append(parts, strconv.Itoa(node.Val)) walk(node.Left) walk(node.Right) } walk(root) return strings.Join(parts, ",")}
func (codec *Codec) deserialize(data string) *TreeNode { tokens := strings.Split(data, ",") position := 0 var read func() *TreeNode read = func() *TreeNode { // Read in the same order as the writer wrote. token := tokens[position] position++ if token == "#" { return nil } value, _ := strconv.Atoi(token) node := &TreeNode{Val: value} // The left subtree was written first, so read it first. // Its own "#" marks tell us where it ends. node.Left = read() node.Right = read() return node } return read()}// Pre-order with a marker for every empty spot. The markers fix the// shape, so one list is enough to rebuild the tree. n steps each way.function serialize(root: TreeNode | null): string { const parts: string[] = []; const walk = (node: TreeNode | null): void => { // "#" records an empty spot, so the shape is kept. if (node === null) { parts.push("#"); return; } // Node first, then its subtrees: the reader meets the // root of each part before the part itself. parts.push(String(node.val)); walk(node.left); walk(node.right); }; walk(root); return parts.join(",");}
function deserialize(data: string): TreeNode | null { const tokens = data.split(","); let position = 0; const read = (): TreeNode | null => { // Read in the same order as the writer wrote. const token = tokens[position++]; if (token === "#") return null; const node = new TreeNode(Number(token)); // The left subtree was written first, so read it first. // Its own "#" marks tell us where it ends. node.left = read(); node.right = read(); return node; }; return read();}1 Find the work that repeats
There is no loop. Both functions repeat through recursion, so we count the calls. A serialize call writes one token. A deserialize call reads one token. Each is a fixed job, so each call counts as 1 step. Splitting and joining the string touch each token once more.
- def serialize(node, parts):
- if node is None:
- parts.append("#"); return
- parts.append(str(node.val))
- serialize(node.left, parts)
- serialize(node.right, parts)
- def deserialize(tokens):
- token = next(tokens)
- if token == "#": return None
- node = TreeNode(int(token))
- node.left = deserialize(tokens)
- node.right = deserialize(tokens)
- return node
2 Count the runs on Example 1
Example 1 has 4 nodes and 5 empty spots. Serialize makes one call for each of them and writes 9 tokens. Deserialize reads the same 9 tokens, one call each.
9 + 9 = 18 calls.
nodes empty spots calls serialize 4 5 9 deserialize 4 5 9 total 18 3 Write the count for any n
A binary tree with n nodes has n + 1 empty spots, so the string has 2n + 1 tokens. Each direction makes one call per token:
(2n + 1) + (2n + 1) = 4n + 2 callsCheck it with Example 1: 4 × 4 + 2 = 18. The join and the split add 2n + 1 steps each, which only changes the constant.
4 Keep only the part that grows fastest
- Drop the + 2. A fixed amount does not change the growth.
- Drop the 4. When n doubles, 4n doubles too.
What is left is n.
So the time is O(n).
S Space: count the extra memory the same way
The list of tokens has 2n + 1 items, and the string holds them all. Each value has at most 5 characters, so the string grows with n. The call stack is at most h deep, where h is the height, and h is at most n. So the space is O(n).
parts / tokens (2n + 1 items)call stack (h frames)
Tested at the limits
Section titled “Tested at the limits”2,000 random trees of 0 to 12 nodes, in every shape, with values like -1,000, -10, 0 and 42. Pre-order with markers wrote each tree to text, a new Codec read it back, and the tree came back exactly the same every time.
| Largest inputs | Result |
|---|---|
| 10,000 nodes (the most allowed), balanced, written and read back. Pre-order with markers. | 0.007 s, and about 2 MB of extra memory: the text, its pieces and the new tree, which is the O(n) space. |
| 10,000 nodes in one long line, written and read back. Pre-order with markers. | 0.009 s, and about 3 MB of extra memory: the same, plus 10,000 calls that wait at once. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.