-
Notifications
You must be signed in to change notification settings - Fork 273
Save and Restore the Tree
TIP103 Unit 12 Session 2 (Click for link to problem statements)
A binary tree must be written to a string so it can be sent over the network and rebuilt exactly. Implement both directions: serialize turns a tree into a string, and deserialize rebuilds the identical tree from that string.
You may design the string format however you like, as long as a round trip reproduces the original tree.
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):
pass
def deserialize(self, data):
pass- 💡 Difficulty: Hard
- ⏰ Time to complete: 30-40 mins
- 🛠️ Topics: Binary Trees, Depth-First Search (DFS), Preorder Traversal, String Encoding
Understand what the interviewer is asking for by using test cases and questions about the problem.
- Established a set (2-3) of test cases to verify their own solution later.
- Established a set (1-2) of edge cases to verify their solution handles complexities.
- Have fully understood the problem and have no clarifying questions.
- Have you verified any Time/Space Constraints for this problem?
- Q: Do we have to produce one specific string format?
- A: No. Any format is acceptable as long as
deserialize(serialize(root))reconstructs a tree identical to the original, node for node.
- A: No. Any format is acceptable as long as
- Q: What must the string capture so the tree can be rebuilt exactly?
- A: Both the node values and the tree's shape. A plain list of values is not enough — we must also record where the missing (
None) children are so the structure is unambiguous.
- A: Both the node values and the tree's shape. A plain list of values is not enough — we must also record where the missing (
- Q: Can the tree be empty, and can values be negative or multi-digit?
- A: Yes to both.
serialize(None)should produce a string thatdeserializeturns back intoNone, and the format needs a delimiter so values like-10or700are not confused with single digits.
- A: Yes to both.
HAPPY CASE
Input:
1
/ \
2 3
/ \
4 5
root = TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5)))
data = codec.serialize(root)
rebuilt = codec.deserialize(data)
Output: codec.serialize(rebuilt) == data evaluates to True
Explanation: Serializing the tree, deserializing the string, and serializing again produces the exact same string, so the round trip reproduced the original tree.
EDGE CASE
Input: root = None (an empty tree)
Output: deserialize(serialize(None)) returns None
Explanation: The codec must handle a tree with no nodes at all.
Input: root = TreeNode(1, None, TreeNode(2, None, TreeNode(3))) (a right-skewed tree)
Output: The rebuilt tree is identical — every left child is None and the values 1, 2, 3 chain down the right side.
Explanation: Skewed trees expose formats that fail to record missing children; without None markers, this tree is indistinguishable from other shapes with the same values.
Match what this problem looks like to known categories of problems, e.g. Linked List or Dynamic Programming, and strategies or patterns in those categories.
For Binary Tree Serialization Problems, we can consider the following approaches:
-
Preorder DFS with None markers: Walk the tree root-left-right, writing each value and writing a sentinel (like
"N") for every missing child. Preorder is ideal because the first token is always the subtree's root, which makes rebuilding a natural recursion. - BFS (level-order) with None markers: Encode the tree level by level using a queue; also valid, but requires more bookkeeping when rebuilding.
-
Recursion / Divide and Conquer: Both
serializeanddeserializedecompose into "handle the root, then recurse on the left subtree, then the right subtree."
Plan the solution with appropriate visualizations and pseudocode.
General Idea:
Serialize with a preorder DFS, emitting each node's value and the sentinel "N" for every None child, joined by commas. Because every missing child is recorded, the string encodes the tree's exact shape. To deserialize, split the string into tokens and consume them left to right with the same preorder recursion: read a token, and if it is "N" return None; otherwise create a node and recursively build its left then right subtree. The token order guarantees each recursive call consumes exactly the tokens for its own subtree.
serialize(root):
1) Create an empty list of tokens.
2) DFS from the root in preorder:
a) If the node is None, append "N" and return.
b) Append the node's value as a string.
c) Recurse on the left child, then the right child.
3) Join the tokens with commas and return the string.
deserialize(data):
1) Split the string on commas and create an iterator over the tokens.
2) Define a recursive build function:
a) Take the next token.
b) If it is "N", return None.
c) Otherwise create a TreeNode from the token's integer value.
d) Recursively build the node's left subtree, then its right subtree.
e) Return the node.
3) Return the result of the first build call.
- Serializing only the values without
Nonemarkers — different tree shapes then produce the same string and cannot be told apart. - Forgetting a delimiter between values, so
1,2and12become ambiguous, or negative signs break single-character parsing. - Building the left and right subtrees in a different order in
deserializethan they were written inserialize. - Re-scanning the token list with an index reset in each recursive call instead of consuming tokens once, in order.
- Not handling the empty tree, causing
deserializeto crash on the string"N".
Implement the code to solve the algorithm.
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):
# Preorder DFS; record None children as "N" so the shape is unambiguous
parts = []
def dfs(node):
if node is None:
parts.append("N")
return
parts.append(str(node.val))
dfs(node.left)
dfs(node.right)
dfs(root)
return ",".join(parts)
def deserialize(self, data):
# Consume tokens in the same preorder they were written
tokens = iter(data.split(","))
def build():
token = next(tokens)
if token == "N":
return None
node = TreeNode(int(token))
node.left = build()
node.right = build()
return node
return build()Review the code by running specific example(s) and recording values (watchlist) of your code's variables along the way.
-
Input:
root = TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5)))-
serializevisits the nodes in preorder: 1, 2, (N, N), 3, 4, (N, N), 5, (N, N). -
databecomes"1,2,N,N,3,4,N,N,5,N,N". -
deserializereads1(root), then2(left child), thenN,N(2 is a leaf), then3(right child), then4withN,N, then5withN,N— rebuilding the exact original shape. -
codec.serialize(rebuilt) == data→ Output:True, matching the expected example output.
-
-
Input:
root = None-
serializereturns"N", anddeserialize("N")reads the single token and returnsNone. The round trip holds for the empty tree.
-
Evaluate the performance of your algorithm and state any strong/weak or future potential work.
Assume N is the number of nodes in the tree and H is its height.
-
Time Complexity:
O(N)for bothserializeanddeserialize— each node (and eachNonechild slot, of which there areN + 1) is visited exactly once. -
Space Complexity:
O(N)for the token list and output string, plusO(H)for the recursion stack, which degrades toO(N)for a skewed tree.