266 lines
12 KiB
YAML
266 lines
12 KiB
YAML
title: Serialize and Deserialize Binary Tree
|
|
slug: serialize-and-deserialize-binary-tree
|
|
difficulty: hard
|
|
leetcode_id: 297
|
|
leetcode_url: https://leetcode.com/problems/serialize-and-deserialize-binary-tree/
|
|
categories:
|
|
- trees
|
|
- strings
|
|
patterns:
|
|
- bfs
|
|
- dfs
|
|
- tree-traversal
|
|
|
|
function_signature: "class Codec"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { root: [1, 2, 3, null, null, 4, 5] }
|
|
expected: [1, 2, 3, null, null, 4, 5]
|
|
- input: { root: [] }
|
|
expected: []
|
|
hidden:
|
|
- input: { root: [1] }
|
|
expected: [1]
|
|
- input: { root: [1, 2] }
|
|
expected: [1, 2]
|
|
- input: { root: [1, null, 2] }
|
|
expected: [1, null, 2]
|
|
- input: { root: [1, 2, 3, 4, 5, 6, 7] }
|
|
expected: [1, 2, 3, 4, 5, 6, 7]
|
|
|
|
description: |
|
|
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 link to be reconstructed later in the same or another computer environment.
|
|
|
|
Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure.
|
|
|
|
**Clarification:** The input/output format is the same as how LeetCode serializes a binary tree. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself.
|
|
|
|
constraints: |
|
|
- The number of nodes in the tree is in the range `[0, 10^4]`
|
|
- `-1000 <= Node.val <= 1000`
|
|
|
|
examples:
|
|
- input: "root = [1,2,3,null,null,4,5]"
|
|
output: "[1,2,3,null,null,4,5]"
|
|
explanation: "The tree is serialized to a string representation and then deserialized back to the original tree structure."
|
|
- input: "root = []"
|
|
output: "[]"
|
|
explanation: "An empty tree serializes to an empty representation."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Think of serialization like writing directions to recreate a sculpture. You need to capture enough information that someone else (or a computer) can rebuild the exact same structure from your description alone.
|
|
|
|
For a binary tree, the challenge is that we need to encode not just the *values* of nodes, but also the *structure* — which nodes are children of which, and where the tree has missing children (null positions). Without encoding the nulls, we couldn't distinguish between different tree shapes that have the same values.
|
|
|
|
Imagine you're describing a family tree over the phone. You might say: "The root is 1. Their left child is 2, right child is 3. Node 2 has no children. Node 3's left child is 4, right child is 5." This level-by-level description is essentially **BFS serialization**.
|
|
|
|
Alternatively, you could describe it depth-first: "Start at 1, go left to 2. Node 2 has no left child (null), no right child (null). Back to 1, go right to 3. Node 3's left is 4..." This is **preorder DFS serialization**.
|
|
|
|
Both approaches work because they capture the complete structure. The key insight is that by including null markers, we can uniquely reconstruct any binary tree — even ones that aren't complete or balanced.
|
|
|
|
approach: |
|
|
We present two approaches: **Preorder DFS** (recursive and elegant) and **Level-Order BFS** (iterative and intuitive).
|
|
|
|
### Approach 1: Preorder DFS
|
|
|
|
**Step 1: Serialization — Preorder traversal with null markers**
|
|
|
|
- Visit the current node and append its value to the result
|
|
- Use a delimiter (comma) between values
|
|
- Use a special marker (e.g., `"null"` or `"#"`) for null nodes
|
|
- Recursively serialize left subtree, then right subtree
|
|
|
|
|
|
|
|
**Step 2: Deserialization — Rebuild from preorder sequence**
|
|
|
|
- Split the serialized string by delimiter to get a list of values
|
|
- Use an iterator or index to track position in the list
|
|
- For each value: if it's null, return `None`; otherwise create a node
|
|
- Recursively build left subtree, then right subtree
|
|
- The preorder property ensures we process nodes in the correct order
|
|
|
|
|
|
|
|
### Approach 2: Level-Order BFS
|
|
|
|
**Step 1: Serialization — Level-by-level traversal**
|
|
|
|
- Use a queue starting with the root
|
|
- For each node: append its value (or null marker) to result
|
|
- Add children to queue (even if null, we still record them)
|
|
|
|
|
|
|
|
**Step 2: Deserialization — Rebuild level by level**
|
|
|
|
- Parse the first value as root
|
|
- Use a queue of parent nodes
|
|
- For each parent, the next two values in the list are its left and right children
|
|
- Add non-null children to the queue for processing their children
|
|
|
|
|
|
|
|
Both approaches have the same complexity, but DFS is often more concise while BFS matches LeetCode's standard format.
|
|
|
|
common_pitfalls:
|
|
- title: Forgetting to Encode Null Children
|
|
description: |
|
|
Without null markers, you cannot distinguish between different tree structures. For example, consider these two trees:
|
|
|
|
- Tree A: root=1, left=2, right=null
|
|
- Tree B: root=1, left=null, right=2
|
|
|
|
If you only serialize non-null values as "1,2", both trees produce the same string! By encoding nulls, Tree A becomes "1,2,null" and Tree B becomes "1,null,2".
|
|
wrong_approach: "Only serialize non-null node values"
|
|
correct_approach: "Include null markers for missing children"
|
|
|
|
- title: Delimiter Conflicts with Node Values
|
|
description: |
|
|
If node values can contain your delimiter character, parsing breaks. For example, if values could be strings containing commas and you use comma as delimiter.
|
|
|
|
For this problem, node values are integers in `[-1000, 1000]`, so comma is safe. But in general, consider escaping or using a delimiter that can't appear in values.
|
|
wrong_approach: "Using a delimiter that could appear in node values"
|
|
correct_approach: "Choose a delimiter that's guaranteed not to appear in values"
|
|
|
|
- title: Off-by-One Errors in BFS Deserialization
|
|
description: |
|
|
In BFS deserialization, it's easy to lose track of which values correspond to which parent's children. The pattern is: for each parent node dequeued, consume the next two values for its left and right children.
|
|
|
|
A common mistake is processing children before their parent is dequeued, or consuming too many/few values per parent.
|
|
wrong_approach: "Inconsistent pairing of parents to children values"
|
|
correct_approach: "Strictly consume two values (left, right) per dequeued parent"
|
|
|
|
- title: Not Handling Empty Tree
|
|
description: |
|
|
The empty tree (null root) is a valid input. Your serialization should produce a recognizable empty representation, and deserialization should correctly return `None`.
|
|
|
|
Forgetting this edge case leads to errors like trying to access `root.val` when root is `None`.
|
|
|
|
key_takeaways:
|
|
- "**Null markers are essential**: They encode the tree's *structure*, not just its values — this is what distinguishes serialization from simple traversal"
|
|
- "**Preorder + nulls = unique tree**: Preorder traversal with null markers uniquely identifies any binary tree, enabling perfect reconstruction"
|
|
- "**BFS matches intuition**: Level-order serialization is often easier to visualize and debug since it matches how we draw trees"
|
|
- "**This pattern appears everywhere**: Serialization concepts apply to JSON parsing, protocol buffers, database storage, and network transmission of complex data structures"
|
|
|
|
time_complexity: "O(n). Both serialization and deserialization visit each node exactly once, where `n` is the number of nodes in the tree."
|
|
space_complexity: "O(n). The serialized string stores `n` node values plus null markers. The recursion stack (DFS) or queue (BFS) can hold up to O(n) nodes in the worst case (skewed tree)."
|
|
|
|
solutions:
|
|
- approach_name: Preorder DFS
|
|
is_optimal: true
|
|
code: |
|
|
class TreeNode:
|
|
def __init__(self, val=0, left=None, right=None):
|
|
self.val = val
|
|
self.left = left
|
|
self.right = right
|
|
|
|
class Codec:
|
|
def serialize(self, root: TreeNode | None) -> str:
|
|
"""Encodes a tree to a single string using preorder traversal."""
|
|
result = []
|
|
|
|
def dfs(node: TreeNode | None) -> None:
|
|
if node is None:
|
|
result.append("null")
|
|
return
|
|
# Preorder: process current node first
|
|
result.append(str(node.val))
|
|
# Then recursively serialize left and right subtrees
|
|
dfs(node.left)
|
|
dfs(node.right)
|
|
|
|
dfs(root)
|
|
return ",".join(result)
|
|
|
|
def deserialize(self, data: str) -> TreeNode | None:
|
|
"""Decodes your encoded data to tree."""
|
|
values = iter(data.split(","))
|
|
|
|
def build() -> TreeNode | None:
|
|
val = next(values)
|
|
if val == "null":
|
|
return None
|
|
# Create node and recursively build its subtrees
|
|
node = TreeNode(int(val))
|
|
node.left = build() # Next values are left subtree
|
|
node.right = build() # Followed by right subtree
|
|
return node
|
|
|
|
return build()
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each node is visited once during both serialization and deserialization.
|
|
|
|
**Space Complexity:** O(n) — The serialized string has O(n) elements. Recursion depth is O(h) where h is tree height, which is O(n) in the worst case (skewed tree).
|
|
|
|
The preorder approach is elegant because the serialization order naturally matches the deserialization order. When we deserialize, the first value is always the root, followed by its complete left subtree, then its complete right subtree. This recursive structure makes the code concise and easy to reason about.
|
|
|
|
- approach_name: Level-Order BFS
|
|
is_optimal: true
|
|
code: |
|
|
from collections import deque
|
|
|
|
class TreeNode:
|
|
def __init__(self, val=0, left=None, right=None):
|
|
self.val = val
|
|
self.left = left
|
|
self.right = right
|
|
|
|
class Codec:
|
|
def serialize(self, root: TreeNode | None) -> str:
|
|
"""Encodes a tree to a single string using level-order traversal."""
|
|
if root is None:
|
|
return "null"
|
|
|
|
result = []
|
|
queue = deque([root])
|
|
|
|
while queue:
|
|
node = queue.popleft()
|
|
if node is None:
|
|
result.append("null")
|
|
else:
|
|
result.append(str(node.val))
|
|
# Add children to queue (including None for null markers)
|
|
queue.append(node.left)
|
|
queue.append(node.right)
|
|
|
|
return ",".join(result)
|
|
|
|
def deserialize(self, data: str) -> TreeNode | None:
|
|
"""Decodes your encoded data to tree."""
|
|
values = data.split(",")
|
|
if values[0] == "null":
|
|
return None
|
|
|
|
# Create root from first value
|
|
root = TreeNode(int(values[0]))
|
|
queue = deque([root])
|
|
i = 1 # Index for remaining values
|
|
|
|
while queue and i < len(values):
|
|
parent = queue.popleft()
|
|
|
|
# Left child is next value
|
|
if values[i] != "null":
|
|
parent.left = TreeNode(int(values[i]))
|
|
queue.append(parent.left)
|
|
i += 1
|
|
|
|
# Right child is the value after that
|
|
if i < len(values) and values[i] != "null":
|
|
parent.right = TreeNode(int(values[i]))
|
|
queue.append(parent.right)
|
|
i += 1
|
|
|
|
return root
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each node is visited once during both operations.
|
|
|
|
**Space Complexity:** O(n) — The queue can hold up to O(n/2) ≈ O(n) nodes at the widest level. The serialized string is O(n).
|
|
|
|
The BFS approach processes the tree level by level, which produces output matching LeetCode's standard tree format. During deserialization, we pair each parent with its two children in order, using a queue to track which parents still need children assigned. This approach is more intuitive for those who think in terms of tree levels.
|