Write a function to serialize a binary tree into a string, and another function to deserialize that string back into the original binary tree structure.
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
Serializing is like saving a document: flatten a structured thing into a flat stream of bytes. Deserializing is reopening it: rebuild the structure from that stream. The catch with trees is recording the empty spots too, or you cannot tell the shape apart on reload.
The primary challenge of this problem lies in how we serialize the tree into a string since this will determine if it’s possible to reconstruct the tree using this string alone.
Let's first decide on a traversal strategy because the method we use to serialize the tree will impact how we deserialize the string. Two options:
Use BFS to serialize the tree level by level.
Use DFS. In this case, we’d need to choose between inorder, preorder, and postorder traversal.
There’s flexibility in choosing the traversal algorithm because serializing with a specific traversal method allows us to rebuild (deserialize) the tree using the same traversal algorithm.
Serialization
An important piece of information needed in our serialized string is the node values. In addition, we’ll need to ensure we can identify the root node's value, since this is the first node to create when we deserialize the string.
As such, a traversal algorithm like preorder traversal is a good choice because it processes the root node first, then the left subtree, and finally the right subtree. This ensures the first value in our serialized string is the root node's value.
So, let’s try serializing the following binary tree using preorder traversal, separating each node with a comma:
The issue with this serialization is that it doesn't guarantee we can reconstruct the exact original tree from its serialized string representation. This is because the string could represent multiple different trees, which means preorder deserialization could result in the creation of an invalid tree:
This is because the string is missing crucial information: null child nodes. For instance, after placing the root node with a value of 5 in the above example, we don't yet know where to place the node of value 9, which is the next value in the string. That is, we can't determine whether node 9 should be the left or right child of node 5:
If the string indicates where the null child nodes are, we can correctly deserialize the tree because we have a complete representation of the tree’s structure. Let's use the character '#' to represent a null node:
Deserialization
To deserialize a string created with preorder traversal, we also need to use preorder traversal to reconstruct the tree.
The first step is to split the string using the comma delimiter, so each node value and ‘#’ is in a list:
The first value in the list is the root node of the tree:
Starting from this root value, recursively construct the tree node by node using preorder traversal. Each new node will be created with the next value in the list of preorder values. Whenever we encounter a ‘#’, we return null. The code snippet for this:
# Helper function to construct the tree using preorder traversal.
def build_tree(values: List[str]) -> TreeNode:
val = next(values)
# Base case: '#' indicates a null node.
if val == '#':
return None
# Use preorder traversal to create the current node first, then the left and
# right subtrees.
node = TreeNode(int(val))
node.left = build_tree(values)
node.right = build_tree(values)
return node
Follow-up: what if you must use a different traversal algorithm?
It’s possible to serialize and deserialize a binary tree using other traversal algorithms than preorder traversal. Let’s explore some alternatives.
When we serialize the tree using postorder traversal, we get the following string (ignoring the null nodes in this discussion to focus on the node values and their order):
As we see, one big difference is that the root node will be the final value in the string, since postorder traversal processes the left subtree, then the right subtree, and finally the root node.
During deserialization, we’d build the tree by iterating through the node values from right to left instead of left to right, since the root value is at the right. In addition, we’d need to create each node’s right subtree before we create its left subtree, as we go through the string in reverse.
A lot more care needs to be taken when serializing a tree using inorder traversal. The main reason is that it’s unclear where the root node of the tree is in the string, and where the root node of each subtree is, as we can see below:
This doesn’t make it impossible to use inorder traversal. It just means significantly more information needs to be provided in the serialized string to make deserialization possible. In particular, we need to include details about which value serves as the root node for each subtree as we iterate through them.
BFS starts processing each node from the root of the tree, and traverses through it level by level, and from left to right. Serializing the tree using BFS gives the following order of values:
Similarly to preorder traversal, we just need to follow the exact traversal order for reconstructing the tree when deserializing the string.
In the following implementation, we opt for preorder traversal for serialization and deserialization.
from ds import TreeNode
def serialize(root: TreeNode) -> str:
# Perform a preorder traversal to add node values to a list, then convert the
# list to a string.
serialized_list = []
preorder_serialize(root, serialized_list)
# Convert the list to a string and separate each value using a comma
# delimiter.
return ','.join(serialized_list)
# Helper function to perform serialization through preorder traversal.
def preorder_serialize(node, serialized_list) -> None:
# Base case: mark null nodes as '#'.
if node is None:
serialized_list.append('#')
return
# Preorder traversal processes the current node first, then the left and right
# children.
serialized_list.append(str(node.val))
preorder_serialize(node.left, serialized_list)
preorder_serialize(node.right, serialized_list)
def deserialize(data: str) -> TreeNode:
# Obtain the node values by splitting the string using the comma delimiter.
node_values = iter(data.split(','))
return build_tree(node_values)
# Helper function to construct the tree using preorder traversal.
def build_tree(values: List[str]) -> TreeNode:
val = next(values)
# Base case: '#' indicates a null node.
if val == '#':
return None
# Use preorder traversal processes the current node first, then the left and
# right children.
node = TreeNode(int(val))
node.left = build_tree(values)
node.right = build_tree(values)
return nodevoid serPre(TreeNode* n, string& s) {
if (n == nullptr) { // null child marker
s += "# ";
return;
}
s += to_string(n->val) + " ";
serPre(n->left, s);
serPre(n->right, s);
}
string serialize(TreeNode* root) {
string s;
serPre(root, s);
while (!s.empty() && s.back() == ' ') { // trim trailing space
s.pop_back();
}
return s;
}
TreeNode* deserRec(vector<string>& t, int& i) {
string v = t[i];
i++;
if (v == "#") { // null marker
return nullptr;
}
TreeNode* n = new TreeNode(stoi(v));
n->left = deserRec(t, i); // same preorder as serialize
n->right = deserRec(t, i);
return n;
}
TreeNode* deserialize(string data) {
vector<string> t;
string cur;
for (char c : data) { // split the string on spaces
if (c == ' ') {
if (!cur.empty()) {
t.push_back(cur);
cur.clear();
}
} else {
cur += c;
}
}
if (!cur.empty()) {
t.push_back(cur);
}
if (t.empty()) {
return nullptr;
}
int i = 0;
return deserRec(t, i);
}void serPre(TreeNode n, StringBuilder s) {
if (n == null) { s.append("# "); return; }
s.append(n.val).append(" ");
serPre(n.left, s);
serPre(n.right, s);
}
String serialize(TreeNode root) {
StringBuilder s = new StringBuilder(); serPre(root, s);
return s.toString().trim();
}
int dIdx;
TreeNode deserRec(String[] t) {
String v = t[dIdx++];
if (v.equals("#")) return null;
TreeNode n = new TreeNode(Integer.parseInt(v));
n.left = deserRec(t);
n.right = deserRec(t);
return n;
}
TreeNode deserialize(String data) {
if (data.isEmpty()) return null;
String[] t = data.split(" ");
dIdx = 0;
return deserRec(t);
}Time complexity: The time complexity of both serialize and deserialize is , where denotes the number of nodes in the tree. This is because we visit each of the nodes in the binary tree exactly once during preorder traversal. The serialize function additionally converts the serialized list to a string, which also takes time.
Space complexity: The space complexity of both serialize and deserialize is due to the space taken up by the recursive call stack, which can grow as large as the height of the binary tree. The largest possible height of a binary tree is 𝑛.
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
Serialize (preorder, # for empties):
| visit | emit | string so far |
|---|---|---|
| 1 | 1 | 1 |
| 2, then its two empties | 2 # # | 1 2 # # |
| 3 | 3 | 1 2 # # 3 |
| 4 (+ empties) | 4 # # | 1 2 # # 3 4 # # |
| 5 (+ empties) | 5 # # | 1 2 # # 3 4 # # 5 # # |
Deserialize reads those tokens in the same order: 1 → make node, build left from 2 # # (leaf 2), build right from 3 4 # # 5 # # (node 3 with children 4, 5). Identical tree rebuilt. ✓
Serialize → "1 # 2 3 # # #". The leading # right after 1 records that 1 has no left child.
| token read | action |
|---|---|
| 1 | make node 1; build its left next |
| # | node 1’s left = None (the recorded gap) |
| 2 | node 1’s right = node 2; build 2’s left next |
| 3 | node 2’s left = node 3; build 3’s children |
| #, # | node 3’s left and right = None |
| # | node 2’s right = None → done |
The single # after 1 is what tells deserialize the child is on the right, not the left. Drop it and the shape would be lost. Rebuilt tree matches exactly. ✓
iter(...) + next(...) stream the tokens so the recursion pulls them in preorder. " ".join builds the string.
An index i passed by reference walks the token vector. We trim the trailing space after building the serialized string.
split(" ") makes the token array; a field dIdx is the shared read cursor. trim() removes the trailing space. Same preorder as the others.