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:
serializeanddeserialize - The
serializemethod takes a binary tree root node and returns a string - The
deserializemethod 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 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)) 861/** 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)); 1011/** 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)); 1011/** 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 */ 112Solution 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.
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
joinoperation takesO(m)wheremis 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
splitoperation takesO(k)wherekis 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/2nodes) - The result list
ansstores 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
valsarray after splitting storesO(n)elements - The queue stores at most
O(n)nodes - The reconstructed tree itself contains
nnodes, 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
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
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!