297. Serialize and Deserialize Binary Tree
Problem Description
This problem asks you to design an algorithm that can convert a binary tree into a string representation (serialization) and then convert that string back into the original binary tree structure (deserialization).
Serialization means taking a binary tree data structure and converting it into a string format that can be easily stored or transmitted.
Deserialization means taking that string and reconstructing the exact same binary tree from it.
The key requirements are:
- You need to implement two methods:
serialize
anddeserialize
- The
serialize
method takes a binary tree root node and returns a string - The
deserialize
method takes a string and returns the root of the reconstructed binary tree - After serializing and then deserializing a tree, you should get back the exact same tree structure
You have complete freedom in choosing how to represent the tree as a string. The only constraint is that your serialization method must produce a string that your deserialization method can correctly interpret to rebuild the original tree.
For example, if you have a tree:
1 / \ 2 3 / \ 4 5
Your serialize method might produce a string like "1,2,3,#,#,4,5,#,#,#,#"
(where #
represents null nodes), and your deserialize method should be able to take this string and reconstruct the exact same tree structure.
The solution uses level-order traversal (BFS) to serialize the tree, including null nodes as special markers (#
), and then reconstructs the tree by processing the serialized values in the same level-order sequence.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph - specifically, it's a connected acyclic graph where each node has at most two children.
Is it a tree?
- Yes: The problem explicitly deals with a binary tree data structure.
DFS
- We arrive at DFS as the suggested approach for tree problems.
Conclusion: The flowchart suggests using a Depth-First Search (DFS) approach for serializing and deserializing the binary tree.
Why DFS fits this problem:
-
Tree Traversal: DFS naturally traverses trees by exploring each branch completely before backtracking, which is perfect for serialization where we need to visit every node.
-
Recursive Nature: DFS can be implemented recursively, making it elegant for tree problems. For serialization, we can recursively process each subtree.
-
Pre-order/In-order/Post-order Options: DFS offers different traversal orders (pre-order, in-order, post-order) that can be used for serialization. Pre-order is particularly useful as it visits the root first, making deserialization straightforward.
-
Natural Structure Preservation: When using DFS (especially pre-order), the serialized format naturally preserves the tree structure - we process parent nodes before children, which aligns with how we'd reconstruct the tree.
Note: While the provided solution uses BFS (level-order traversal), DFS is equally valid and often simpler to implement for this problem. Both approaches work well for tree serialization/deserialization.
Intuition
To serialize and deserialize a binary tree, we need to find a way to capture all the information about the tree structure in a string format that we can later decode.
The key insight is that we need to record two things:
- The values of all nodes
- The structure/relationships between nodes (which nodes are children of which parent)
When we traverse a tree, we visit nodes in a specific order. If we can record this order along with markers for missing nodes (null children), we can reconstruct the exact same tree structure later.
Think about how you would describe a tree to someone over the phone. You might say: "Start with node 1, its left child is 2, its right child is 3. Node 2 has no children. Node 3's left child is 4, right child is 5..." This is essentially what serialization does - it creates a systematic description of the tree.
The challenge is handling null nodes. In a complete binary tree, we know exactly where each node should be. But in an incomplete tree, we need to mark where nodes are missing. This is why we use a special character like #
to represent null nodes.
Whether we use BFS (level-order) or DFS (pre/in/post-order), the principle remains the same:
- For serialization: Visit nodes in a systematic order and record their values, using a special marker for nulls
- For deserialization: Read the values in the same order and reconstruct the tree following the same traversal pattern
BFS is intuitive because it processes the tree level by level, making it easy to understand which nodes are siblings and which are parent-child. We process all nodes at depth 0, then depth 1, then depth 2, and so on. When deserializing, we can assign children to parents in the same level-by-level manner.
The string format "1,2,3,#,#,4,5"
tells us: root is 1, its children are 2 and 3, node 2 has no children (marked by #,#
), node 3 has children 4 and 5. This information is sufficient to perfectly reconstruct the original tree.
Solution Approach
The solution uses Level Order Traversal (BFS) to serialize and deserialize the binary tree. Here's how each part works:
Serialization Process
- Initialize a queue with the root node and an empty result list
- Process nodes level by level:
- Dequeue a node from the front
- If the node exists, append its value to the result and enqueue both its children (even if they're
None
) - If the node is
None
, append"#"
as a placeholder
- Join all values with commas to create the final serialized string
q = deque([root])
ans = []
while q:
node = q.popleft()
if node:
ans.append(str(node.val))
q.append(node.left) # Add even if None
q.append(node.right) # Add even if None
else:
ans.append("#")
return ",".join(ans)
Deserialization Process
- Split the string by commas to get an array of values
- Create the root node from the first value
- Use a queue to track nodes that need children assigned
- Process values in pairs (left child, right child):
- For each parent node in the queue
- Read the next two values from the array
- If a value is not
"#"
, create a new node and add it to the queue - Assign the nodes as left and right children
vals = data.split(",")
root = TreeNode(int(vals[0]))
q = deque([root])
i = 1
while q:
node = q.popleft()
# Process left child
if vals[i] != "#":
node.left = TreeNode(int(vals[i]))
q.append(node.left)
i += 1
# Process right child
if vals[i] != "#":
node.right = TreeNode(int(vals[i]))
q.append(node.right)
i += 1
Key Data Structures
- Queue (deque): Used for level-order traversal in both serialization and deserialization
- List: Temporarily stores node values during serialization
- String: Final serialized format with comma-separated values
Why This Works
The algorithm maintains a one-to-one correspondence between the serialization and deserialization process:
- During serialization, we process parent nodes before children and record nulls
- During deserialization, we read values in the same order and reconstruct relationships
- The queue ensures we process nodes level by level in both operations
- The
"#"
markers preserve the tree structure by indicating where nodes are missing
This approach has O(n) time complexity for both operations (visiting each node once) and O(n) space complexity (for the queue and result string).
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach. Consider this binary tree:
1 / \ 2 3 / 4
Serialization Process:
-
Initialize: Queue = [1], Result = []
-
Level 0 (root):
- Dequeue node 1
- Add "1" to result → Result = ["1"]
- Enqueue its children → Queue = [2, 3]
-
Level 1:
-
Dequeue node 2
-
Add "2" to result → Result = ["1", "2"]
-
Enqueue its children → Queue = [3, 4, None]
-
Dequeue node 3
-
Add "3" to result → Result = ["1", "2", "3"]
-
Enqueue its children → Queue = [4, None, None, None]
-
-
Level 2:
-
Dequeue node 4
-
Add "4" to result → Result = ["1", "2", "3", "4"]
-
Enqueue its children → Queue = [None, None, None, None, None]
-
Dequeue None (from node 2's right)
-
Add "#" to result → Result = ["1", "2", "3", "4", "#"]
-
No children to enqueue → Queue = [None, None, None, None]
-
-
Continue processing remaining Nones:
- Result = ["1", "2", "3", "4", "#", "#", "#", "#", "#"]
-
Final string:
"1,2,3,4,#,#,#,#,#"
Deserialization Process:
-
Split string: vals = ["1", "2", "3", "4", "#", "#", "#", "#", "#"]
-
Create root: node(1), Queue = [node(1)], index i = 1
-
Process node(1)'s children:
- vals[1] = "2" → Create node(2) as left child
- vals[2] = "3" → Create node(3) as right child
- Queue = [node(2), node(3)], i = 3
-
Process node(2)'s children:
- vals[3] = "4" → Create node(4) as left child
- vals[4] = "#" → No right child
- Queue = [node(3), node(4)], i = 5
-
Process node(3)'s children:
- vals[5] = "#" → No left child
- vals[6] = "#" → No right child
- Queue = [node(4)], i = 7
-
Process node(4)'s children:
- vals[7] = "#" → No left child
- vals[8] = "#" → No right child
- Queue = [], i = 9
-
Result: The original tree is reconstructed:
1 / \ 2 3 / 4
The key insight is that we process nodes in the same order during both serialization and deserialization, maintaining the parent-child relationships through the level-order traversal pattern.
Solution Implementation
1from collections import deque
2from typing import Optional
3
4# Definition for a binary tree node.
5class TreeNode:
6 def __init__(self, x: int):
7 self.val = x
8 self.left: Optional['TreeNode'] = None
9 self.right: Optional['TreeNode'] = None
10
11
12class Codec:
13 def serialize(self, root: Optional[TreeNode]) -> str:
14 """
15 Encodes a tree to a single string using level-order traversal.
16
17 :type root: TreeNode
18 :rtype: str
19 """
20 # Handle empty tree case
21 if root is None:
22 return ""
23
24 # Initialize queue for BFS traversal
25 queue = deque([root])
26 result = []
27
28 # Perform level-order traversal
29 while queue:
30 node = queue.popleft()
31
32 if node:
33 # Add node value to result
34 result.append(str(node.val))
35 # Add children to queue (even if None)
36 queue.append(node.left)
37 queue.append(node.right)
38 else:
39 # Use "#" to represent None nodes
40 result.append("#")
41
42 # Join all values with comma separator
43 return ",".join(result)
44
45 def deserialize(self, data: str) -> Optional[TreeNode]:
46 """
47 Decodes the encoded string back to a binary tree.
48
49 :type data: str
50 :rtype: TreeNode
51 """
52 # Handle empty string case
53 if not data:
54 return None
55
56 # Split the string into individual values
57 values = data.split(",")
58
59 # Create root node from first value
60 root = TreeNode(int(values[0]))
61 queue = deque([root])
62 index = 1
63
64 # Reconstruct tree using BFS
65 while queue:
66 node = queue.popleft()
67
68 # Process left child
69 if values[index] != "#":
70 node.left = TreeNode(int(values[index]))
71 queue.append(node.left)
72 index += 1
73
74 # Process right child
75 if values[index] != "#":
76 node.right = TreeNode(int(values[index]))
77 queue.append(node.right)
78 index += 1
79
80 return root
81
82
83# Your Codec object will be instantiated and called as such:
84# codec = Codec()
85# codec.deserialize(codec.serialize(root))
86
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode(int x) { val = x; }
8 * }
9 */
10public class Codec {
11
12 /**
13 * Serializes a binary tree to a string representation using level-order traversal.
14 * Uses BFS to traverse the tree and represents null nodes with "#".
15 *
16 * @param root The root node of the binary tree to serialize
17 * @return A comma-separated string representation of the tree, or null if tree is empty
18 */
19 public String serialize(TreeNode root) {
20 // Handle empty tree case
21 if (root == null) {
22 return null;
23 }
24
25 // List to store the serialized node values
26 List<String> serializedNodes = new ArrayList<>();
27
28 // Queue for BFS traversal
29 Deque<TreeNode> queue = new LinkedList<>();
30 queue.offer(root);
31
32 // Perform level-order traversal
33 while (!queue.isEmpty()) {
34 TreeNode currentNode = queue.poll();
35
36 if (currentNode != null) {
37 // Add node value to result and enqueue children
38 serializedNodes.add(String.valueOf(currentNode.val));
39 queue.offer(currentNode.left);
40 queue.offer(currentNode.right);
41 } else {
42 // Use "#" to represent null nodes
43 serializedNodes.add("#");
44 }
45 }
46
47 // Join all values with comma separator
48 return String.join(",", serializedNodes);
49 }
50
51 /**
52 * Deserializes a string representation back into a binary tree.
53 * Reconstructs the tree using BFS, processing nodes in the same order they were serialized.
54 *
55 * @param data The comma-separated string representation of the tree
56 * @return The root node of the reconstructed binary tree, or null if data is null
57 */
58 public TreeNode deserialize(String data) {
59 // Handle empty tree case
60 if (data == null) {
61 return null;
62 }
63
64 // Split the serialized string into individual node values
65 String[] nodeValues = data.split(",");
66 int index = 0;
67
68 // Create root node from first value
69 TreeNode root = new TreeNode(Integer.valueOf(nodeValues[index++]));
70
71 // Queue to maintain nodes whose children need to be processed
72 Deque<TreeNode> queue = new ArrayDeque<>();
73 queue.offer(root);
74
75 // Process nodes level by level
76 while (!queue.isEmpty()) {
77 TreeNode currentNode = queue.poll();
78
79 // Process left child
80 if (!"#".equals(nodeValues[index])) {
81 currentNode.left = new TreeNode(Integer.valueOf(nodeValues[index]));
82 queue.offer(currentNode.left);
83 }
84 index++;
85
86 // Process right child
87 if (!"#".equals(nodeValues[index])) {
88 currentNode.right = new TreeNode(Integer.valueOf(nodeValues[index]));
89 queue.offer(currentNode.right);
90 }
91 index++;
92 }
93
94 return root;
95 }
96}
97
98// Your Codec object will be instantiated and called as such:
99// Codec codec = new Codec();
100// codec.deserialize(codec.serialize(root));
101
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode(int x) : val(x), left(NULL), right(NULL) {}
8 * };
9 */
10class Codec {
11public:
12 /**
13 * Encodes a tree to a single string using level-order traversal (BFS).
14 * Null nodes are represented as "#" in the serialized string.
15 * @param root The root of the binary tree to serialize
16 * @return A string representation of the tree
17 */
18 string serialize(TreeNode* root) {
19 // Handle empty tree case
20 if (!root) {
21 return "";
22 }
23
24 // Use queue for level-order traversal
25 queue<TreeNode*> nodeQueue;
26 nodeQueue.push(root);
27 string serializedResult;
28
29 // Process all nodes including nulls
30 while (!nodeQueue.empty()) {
31 TreeNode* currentNode = nodeQueue.front();
32 nodeQueue.pop();
33
34 if (currentNode) {
35 // Append node value and add children to queue
36 serializedResult += to_string(currentNode->val) + " ";
37 nodeQueue.push(currentNode->left);
38 nodeQueue.push(currentNode->right);
39 } else {
40 // Use "#" to represent null nodes
41 serializedResult += "# ";
42 }
43 }
44
45 // Remove trailing space
46 serializedResult.pop_back();
47 return serializedResult;
48 }
49
50 /**
51 * Decodes the encoded string back to a binary tree.
52 * Reconstructs the tree using level-order traversal.
53 * @param data The serialized string representation of the tree
54 * @return The root of the reconstructed binary tree
55 */
56 TreeNode* deserialize(string data) {
57 // Handle empty tree case
58 if (data.empty()) {
59 return nullptr;
60 }
61
62 // Parse the serialized string
63 stringstream stringStream(data);
64 string token;
65
66 // Create root node
67 stringStream >> token;
68 TreeNode* root = new TreeNode(stoi(token));
69
70 // Use queue to track parent nodes for level-order reconstruction
71 queue<TreeNode*> parentQueue;
72 parentQueue.push(root);
73
74 // Process remaining tokens to build tree
75 while (!parentQueue.empty()) {
76 TreeNode* parentNode = parentQueue.front();
77 parentQueue.pop();
78
79 // Process left child
80 stringStream >> token;
81 if (token != "#") {
82 parentNode->left = new TreeNode(stoi(token));
83 parentQueue.push(parentNode->left);
84 }
85
86 // Process right child
87 stringStream >> token;
88 if (token != "#") {
89 parentNode->right = new TreeNode(stoi(token));
90 parentQueue.push(parentNode->right);
91 }
92 }
93
94 return root;
95 }
96};
97
98// Your Codec object will be instantiated and called as such:
99// Codec codec;
100// codec.deserialize(codec.serialize(root));
101
1/**
2 * Definition for a binary tree node.
3 */
4interface TreeNode {
5 val: number;
6 left: TreeNode | null;
7 right: TreeNode | null;
8}
9
10/**
11 * Encodes a tree to a single string using level-order traversal (BFS).
12 * Null nodes are represented as '#' in the serialized string.
13 *
14 * @param {TreeNode | null} root - The root of the binary tree to serialize
15 * @return {string | null} - The serialized string representation of the tree
16 */
17const serialize = function (root: TreeNode | null): string | null {
18 // Handle empty tree case
19 if (root === null) {
20 return null;
21 }
22
23 // Array to store the serialized values
24 const serializedValues: string[] = [];
25
26 // Queue for level-order traversal
27 const queue: (TreeNode | null)[] = [root];
28 let currentIndex = 0;
29
30 // Process nodes in level order
31 while (currentIndex < queue.length) {
32 const currentNode = queue[currentIndex++];
33
34 if (currentNode !== null) {
35 // Add the node value to result
36 serializedValues.push(currentNode.val.toString());
37 // Add children to queue (including null children)
38 queue.push(currentNode.left);
39 queue.push(currentNode.right);
40 } else {
41 // Use '#' to represent null nodes
42 serializedValues.push('#');
43 }
44 }
45
46 // Join all values with comma separator
47 return serializedValues.join(',');
48};
49
50/**
51 * Decodes the encoded string back to a binary tree.
52 * Reconstructs the tree using level-order traversal (BFS).
53 *
54 * @param {string | null} data - The serialized string representation of the tree
55 * @return {TreeNode | null} - The root of the reconstructed binary tree
56 */
57const deserialize = function (data: string | null): TreeNode | null {
58 // Handle empty tree case
59 if (data === null) {
60 return null;
61 }
62
63 // Split the serialized string into individual values
64 const values = data.split(',');
65 let valueIndex = 0;
66
67 // Create the root node from the first value
68 const root: TreeNode = {
69 val: parseInt(values[valueIndex++]),
70 left: null,
71 right: null
72 };
73
74 // Queue to track nodes that need children assignment
75 const queue: TreeNode[] = [root];
76 let queueIndex = 0;
77
78 // Process nodes and assign their children
79 while (queueIndex < queue.length) {
80 const currentNode = queue[queueIndex++];
81
82 // Process left child
83 if (values[valueIndex] !== '#') {
84 currentNode.left = {
85 val: parseInt(values[valueIndex]),
86 left: null,
87 right: null
88 };
89 queue.push(currentNode.left);
90 }
91 valueIndex++;
92
93 // Process right child
94 if (values[valueIndex] !== '#') {
95 currentNode.right = {
96 val: parseInt(values[valueIndex]),
97 left: null,
98 right: null
99 };
100 queue.push(currentNode.right);
101 }
102 valueIndex++;
103 }
104
105 return root;
106};
107
108/**
109 * Your functions will be called as such:
110 * deserialize(serialize(root));
111 */
112
Time and Space Complexity
Time Complexity: O(n)
For the serialize
method:
- We traverse each node in the tree exactly once using BFS (level-order traversal)
- For each node, we perform constant time operations: appending to the queue, appending to the result list, and string conversion
- The final
join
operation takesO(m)
wherem
is the total number of elements in the list, which is at most2n + 1
(including null nodes) - Overall:
O(n)
For the deserialize
method:
- We process each value in the serialized string exactly once
- For each non-null value, we create a node and add it to the queue in constant time
- The
split
operation takesO(k)
wherek
is the length of the string, which is proportional ton
- Overall:
O(n)
Space Complexity: O(n)
For the serialize
method:
- The queue stores at most
O(n)
nodes (in the worst case of a complete binary tree, the last level contains approximatelyn/2
nodes) - The result list
ans
stores all nodes plus null markers, which isO(n)
- The final string representation also takes
O(n)
space - Overall:
O(n)
For the deserialize
method:
- The
vals
array after splitting storesO(n)
elements - The queue stores at most
O(n)
nodes - The reconstructed tree itself contains
n
nodes, takingO(n)
space - Overall:
O(n)
Where n
is the number of nodes in the binary tree.
Common Pitfalls
1. Not Handling Empty Tree Edge Case
A frequent mistake is forgetting to handle the case when the input tree is None
/empty. This causes errors during serialization or deserialization.
Problem Code:
def serialize(self, root):
queue = deque([root]) # Will add None to queue
result = []
while queue:
node = queue.popleft()
result.append(str(node.val)) # Error: None has no attribute 'val'
Solution: Always check for empty tree at the beginning of both methods:
def serialize(self, root):
if root is None:
return ""
# ... rest of the code
def deserialize(self, data):
if not data:
return None
# ... rest of the code
2. Index Out of Bounds in Deserialization
When deserializing, accessing array indices without proper bounds checking can cause IndexError, especially with trailing null nodes.
Problem Code:
while queue:
node = queue.popleft()
# Might exceed array bounds
if values[index] != "#": # IndexError if index >= len(values)
node.left = TreeNode(int(values[index]))
index += 1
Solution: Add bounds checking or ensure serialization doesn't include unnecessary trailing nulls:
while queue and index < len(values):
node = queue.popleft()
if index < len(values) and values[index] != "#":
node.left = TreeNode(int(values[index]))
index += 1
if index < len(values) and values[index] != "#":
node.right = TreeNode(int(values[index]))
index += 1
3. Forgetting to Add None Children to Queue During Serialization
A critical mistake is only adding non-null children to the queue during serialization, which breaks the position mapping needed for deserialization.
Problem Code:
def serialize(self, root):
while queue:
node = queue.popleft()
if node:
result.append(str(node.val))
if node.left: # Wrong: skips None nodes
queue.append(node.left)
if node.right: # Wrong: skips None nodes
queue.append(node.right)
Solution: Always add both children to the queue, even if they're None:
if node:
result.append(str(node.val))
queue.append(node.left) # Add even if None
queue.append(node.right) # Add even if None
else:
result.append("#")
4. Using Wrong Delimiter or Conflicting with Node Values
If node values can contain the delimiter character (e.g., negative numbers with "-" delimiter), the deserialization will fail.
Problem Code:
# If using space as delimiter but values might contain spaces return " ".join(result) # Problematic if values have spaces # Or not handling negative numbers properly values = data.split("-") # Will split "-5" incorrectly
Solution: Use a delimiter that won't appear in node values (comma is safe for integers):
return ",".join(result) # Safe for integer values # For more complex data, consider using JSON or escape sequences
5. Inefficient String Concatenation
Building the serialized string with repeated concatenation instead of joining a list causes O(n²) time complexity.
Problem Code:
def serialize(self, root):
result = ""
while queue:
node = queue.popleft()
if node:
result += str(node.val) + "," # Inefficient string concatenation
else:
result += "#,"
Solution: Collect values in a list and join once at the end:
result = []
while queue:
# ... append to list
result.append(str(node.val))
return ",".join(result) # Single join operation
Which data structure is used in a depth first search?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!