LeetCode 297: Serialize and Deserialize Binary Tree
Imagine you're playing a complex video game and you want to save your progress. The game needs to take all those 3D objects, character stats, and world states (the "tree") and flatten them into a single file (the "string") so you can load it back later. That's exactly what Serialization and Deserialization are all about. ๐พ
๐งฉ The Problem
Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection.
Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your algorithm works; you just need to ensure that a binary tree can be reconstructed to its original structure.
Examples
Example 1:
Input: root = [1,2,3,null,null,4,5]
Output: [1,2,3,null,null,4,5]
1
/ \
2 3
/ \
4 5
Example 2:
Input: root = []
Output: []
Constraints
- The number of nodes in the tree is in the range
[0, 10^4]. -1000 <= Node.val <= 1000.
๐ง How to Think About This
The core challenge is turning a 2D structure (a tree) into a 1D sequence (a string) and back again.
The Ambiguity Problem
A single traversal order (like inorder) is ambiguous. Many different trees can produce the same inorder sequence. For example, [2, 1, 3] could be a balanced tree with 1 at the root, or a skewed tree.
The Solution: Preorder + Null Markers
If we record null pointers as well, a preorder traversal becomes completely unambiguous.
Mental Model:
1
/ \
2 3
/ \
4 5
Preorder (Root โ Left โ Right): Visit 1 โ Visit 2 โ null โ null โ Visit 3 โ Visit 4 โ null โ null โ Visit 5 โ null โ null
Serialized String: "1,2,#,#,3,4,#,#,5,#,#"
The # markers tell us exactly when a subtree ends. No ambiguity! ๐ฏ
๐ Why Preorder Works for Reconstruction
During deserialization, we read the string from left to right:
- Read a value โ Make it the current node.
- Recurse left (the next calls will consume the next tokens for the left subtree).
- Recurse right (the remaining calls will build the right subtree).
The recursion naturally mirrors how we built the string.
Visual Walkthrough
"1, 2, #, #, 3, 4, #, #, 5, #, #"
1is the root. Now, what's on its left?2is the left child of1. Now, what's on its left?#means left of2isNone. Now, what's on its right?#means right of2isNone.2is finished!- Now, what's on the right of
1? 3is the right child of1... and so on.
๐ป The Solution
class Codec:
def serialize(self, root):
"""Encodes a tree to a single string."""
tokens = []
def dfs(node):
if not node:
tokens.append('#')
return
# Root
tokens.append(str(node.val))
# Left
dfs(node.left)
# Right
dfs(node.right)
dfs(root)
return ','.join(tokens)
def deserialize(self, data):
"""Decodes your encoded data to tree."""
# Use an iterator so each recursive call
# automatically advances to the next token
tokens = iter(data.split(','))
def dfs():
val = next(tokens)
if val == '#':
return None
node = TreeNode(int(val))
# Recurse in the same order we serialized
node.left = dfs() # Consumes left subtree tokens
node.right = dfs() # Consumes right subtree tokens
return node
return dfs()The Crucial Trick: iter()
Using a plain list with an index pointer works, but iter() is elegant.
tokens = iter(['1','2','#','#','3'])
next(tokens) # '1' โ each call advances automatically
next(tokens) # '2'Each recursive dfs() call calls next(), so the iterator acts as a shared pointer that moves forward across all recursive levels. No manual index bookkeeping needed! ๐ช
Results
Runtime: 76 ms โ Beats 95.31%
Memory: 22.39 MB โ Beats 88.02%
โฑ๏ธ Complexity Analysis
| Phase | Time Complexity | Space Complexity |
|---|---|---|
| Serialize | ||
| Deserialize | stack depth |
Since we visit every node exactly once, the complexity is linear relative to the number of nodes.
โ ๏ธ Common Mistakes to Avoid
- Skipping Null Markers โ Without
#or some placeholder, you can't tell where a leaf node's children end. - Using Inorder Only โ As mentioned, you need at least two traversals (e.g., preorder + inorder) to reconstruct a tree without null markers. With null markers, one is enough.
- Manual Index Management โ Trying to pass an
indexinteger through recursion often leads to bugs because integers are immutable in Python. Using a list like[0]or aniter()is much safer.
๐ง How to Recognize This Pattern
When you see these keywords in an interview:
- "Serialize" / "Deserialize"
- "Save and load"
- "Flatten a tree"
- "Binary Tree to String"
Think:
DFS (PREORDER) + NULL MARKERS
๐ฏ Interview Tips
- Explain the Ambiguity โ Start by explaining why a simple traversal isn't enough. It shows you understand the depth of the problem.
- Propose the Null Marker โ "To solve the ambiguity, I'll use a special character like
#to represent null nodes." - Mention BFS alternative โ You can also solve this using BFS (Level-order). It's more similar to how LeetCode actually stores trees, but DFS is often more elegant to code recursively.
- Consistency is Key โ The order in which you write (Serialize) must be the exact same order you read (Deserialize).
