Serialize and Deserialize Binary Tree
Encode a binary tree to a string and decode it back exactly.
Open on LeetCode ↗Intuition
Preorder with explicit null markers is unambiguous: the nulls pin down where subtrees end, so the same recursion that wrote the string can replay it. Serialization is a preorder dump; deserialization is the mirror-image recursive read.
To rebuild a tree from a flat string you need the null positions written down — that's the whole insight. Pre-order plus explicit null markers is self-delimiting: the first token is always the root, and recursion consumes exactly its own subtree before returning, so the boundary between left and right needs no separator. Without null markers you'd need two traversals to reconstruct, which is why in-order alone is never enough.
Approach
Serialize: preorder + '#' for null
dfs(node): emit val, recurse left, recurse right; emit # at nulls. Join with commas (handles multi-digit and negatives).
Deserialize: consume in the same order
Read the next token: '#' → None; else make the node and build left then right. An iterator carries the position implicitly.
Why it round-trips
Preorder with nulls encodes the exact shape — no inorder needed, unlike the classic two-traversal reconstruction.
Solution & live demo
Common pitfalls
Omitting the null markers
def dfs(node):
if not node: return
out.append(str(node.val))
dfs(node.left); dfs(node.right)def dfs(node):
if not node:
out.append("#"); return
out.append(str(node.val))
dfs(node.left); dfs(node.right)"1,2" could be 2 as a left child or as a right child — the shape is unrecoverable. The # tokens are what encode structure; they're not padding.
Indexing the token list instead of consuming an iterator
tokens = data.split(",")
def build(i):
...
node.left = build(i + 1)
node.right = build(???)tokens = iter(data.split(","))
def build():
t = next(tokens)
...With an index you'd have to know how many tokens the left subtree swallowed before you could locate the right one — information you only get by rebuilding it. A shared iterator advances as a side effect, so by the time build() returns for the left child the cursor is already sitting on the right child's first token.
Rebuilding children inside the constructor call
return TreeNode(int(t), build(), build())
node = TreeNode(int(t)) node.left = build() node.right = build() return node
Argument evaluation order becomes load-bearing — in a language that evaluates right-to-left the subtrees come back swapped. Sequencing the two calls on their own lines makes the pre-order contract explicit.
Edge cases
Serializes to "#" and decodes to None.
Comma delimiting keeps tokens intact — never parse per character.