DSA sheet · Trees · Serialize & construction pattern
Serialize & Deserialize Binary Tree
This is the last problem of the serialize/construction pattern. We write two functions that work as a pair. Serialize turns a tree into a string. Deserialize takes that same string and rebuilds the exact same tree. The teacher doesn't hand over the final format. She tries a simple string first, shows why it breaks, fixes it, shows the next problem, and fixes that too. By the end we see why every comma and every null marker is needed. The final solution is a preorder traversal on both sides.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the format from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · What you must know before starting
- Part A · Serialize (tree → string) with preorder
- Part B · Deserialize (string → tree) with preorder
- Part C · Revision page
Part 0 · Before starting
What does "serialize" mean?
A tree lives in memory as nodes joined by pointers. You can't directly save pointers to a file or send them over a network. Serializing means writing the tree down as plain text (a string). Deserializing means reading that text back and rebuilding the nodes and pointers. The only rule: deserialize(serialize(tree)) must give back the same tree, with the same values and the same shape.
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = NoneThe three traversals (prerequisite)
- Preorder: root, left, right.
- Inorder: left, root, right.
- Postorder: left, right, root.
The teacher says this video assumes you already know these three traversals well. If not, learn them first.
Part A · Serialize (tree → string) with preorder
LeetCode 297
1The question in simple words
serialize(root): return a string that stores the whole tree.deserialize(data): given that string, rebuild the tree and return its root.- You choose the string format. It only has to round-trip correctly.
Our example tree for the whole page:
1
/ \
2 3
/ \
4 5
2What the constraints tell us
- Nodes: 0 to 10⁴ → the tree can be empty. Then there's nothing to store, and deserializing must give back None.
- n up to 10⁴ means n² = 10⁸, which is right at the TLE line and too slow. So we aim for a linear O(n) solution.
- Values: −1000 to 1000. They fit easily in an int, and we never add or multiply them. But note: values can be negative and have several digits, so in the string a value is more than one character. Remember this in step 4.
3Intuition: write the tree in an order you can replay
We already know three ways to list every node: pre, in and post order. The teacher picks preorder. Its big advantage: the root always comes first. When we read the string back, the first value we meet is the root, then its whole left subtree, then its whole right subtree. That is exactly the order in which we can build nodes: make the root, then build its left, then build its right.
4Building the format from examples (three attempts)
Attempt 1: just write the values → broken
Preorder of the tree: 1, 2, 3, 4, 5. Written together: "12345".
Reading it back, we can't tell where one number ends. Was it 1, 2, 3, 4, 5? Or 12, 3, 45? Or 123, 4, 5? With multi-digit and negative values allowed, this is hopeless.
"1,2,3,4,5". Now each number is clearly separate. (Any character that can't be part of a number would also work. A comma is just the usual choice.)Attempt 2: values with commas → still broken
Now we know the values, but not the shape. Rebuild with preorder: 1 is the root. The next value, 2, must be its left child (preorder goes left after root). Then 3 is… 2's left child? Or 1's right child? The string doesn't say. Nothing tells us that 2 has no children. If we keep assuming "left", we build this wrong tree:
1
/ \
2 3
/ \
4 5 1
/
2
/
3
/
4
/
5The missing piece is: where are the empty spots? Leaves, and nodes with only one child, have empty (None) children, and we never wrote them down.
#. Any non-number character works (a, b, N…).Attempt 3: values + commas + # for None → works
Do preorder again, and write # every time we step into an empty child:
- 1 → write 1. Go left.
- 2 → write 2. 2's left is empty → #. 2's right is empty → #.
- Back to 1, go right: 3 → write 3. Go left.
- 4 → write 4, then #, # (leaf).
- 5 → write 5, then #, # (leaf).
"1,2,#,#,3,4,#,#,5,#,#"Now every node is followed by exactly the information about its two children, so the shape is fixed. In short: commas tell us what the values are, and # tells us where the tree is empty.
→ The video moves on before showing it, so here is the reason. Inorder fails even with
# markers, because two different trees can give the same string. Tree "1 with right child 2" gives #,1,#,2,#, and tree "2 with left child 1" gives #,1,#,2,# too. Inorder never tells you which value is the root. Postorder works (the root is the last token, so you rebuild by reading from the end: root, then right, then left). But preorder is the most natural: the root is the first token, and you read left to right.5Approach steps (serialize)
- If the node is None → return
"#". - Otherwise return: the node's value, a comma, the serialized left subtree, a comma, the serialized right subtree.
6Code (Python)
class Codec:
def serialize(self, root):
if root is None:
return "#"
# root , left , right (preorder)
return str(root.val) + "," + self.serialize(root.left) + "," + self.serialize(root.right)7Code line by line
| line | what it means |
|---|---|
| if root is None: return "#" | Base case. An empty spot is written as the marker. For an empty tree, the whole string is just "#". |
| str(root.val) | Root first (preorder). str() turns the number into text, minus sign included. |
| + "," + self.serialize(root.left) | A separator, then the whole left subtree as text. |
| + "," + self.serialize(root.right) | A separator, then the whole right subtree as text. |
In the video she says "inorder" for a moment while writing this. It's a slip: root, left, right is preorder.
8Dry run
s(x) = serialize(x). Each call waits for its left and right calls to finish, then joins the pieces.
- s(2) → "2," + s(None) + "," + s(None) = "2,#,#".
- s(4) → "4,#,#" and s(5) → "5,#,#" (both leaves).
- s(3) → "3," + "4,#,#" + "," + "5,#,#" = "3,4,#,#,5,#,#".
- s(1) → "1," + "2,#,#" + "," + "3,4,#,#,5,#,#" = "1,2,#,#,3,4,#,#,5,#,#" ✓
9Complexity & remember
- Time O(n): each node (and each None spot) is visited once.
- Space O(n): the output string has n values plus n + 1 markers. The recursion stack is O(height).
+ on strings really O(n)?→ Each
+ copies the pieces, so on a very tall tree the copying adds up (roughly n × height characters). It is fine for these limits. A cleaner Python habit is to append pieces to a list and ",".join(...) once at the end. That version is in the revision part.Part B · Deserialize (string → tree) with preorder
1The question
Input: a string made by our serialize, e.g. "1,2,#,#,3,4,#,#,5,#,#". Output: the root of the rebuilt tree.
2What the constraints tell us
- The string
"#"means an empty tree → return None. Our code handles this without a special check (the very first token is#). - Tokens can be
"-12"or"1000", so we must split on commas, not read one character at a time.
3Intuition: replay the preorder with one moving pointer
Split the string on commas into a list of tokens. Keep one index i that moves forward through the list, and is shared by all the recursive calls. Each call takes the next token:
- If it's
#→ this spot is empty, return None. - Otherwise → make a node with that value, then build its left subtree from the next tokens, then its right subtree.
Because serialize wrote root, left, right, reading back in the same order puts every token in the right place.
4Building the logic from the example
- Token 0 = 1 → the root. Next comes 1's left.
- Token 1 = 2 → 1.left = 2. Next comes 2's left.
- Token 2 = # → 2.left = None. Next is 2's right.
- Token 3 = # → 2.right = None. 2 is finished, go back to 1 and do 1's right.
- Token 4 = 3 → 1.right = 3. Token 5 = 4 → 3.left = 4. Tokens 6, 7 = #, # → 4 is a leaf.
- Token 8 = 5 → 3.right = 5. Tokens 9, 10 = #, # → 5 is a leaf.
- The index has passed the end. Return the root, 1.
i be shared and not passed as a normal argument?→ When the left call finishes, it has used up some tokens (the whole left subtree). The right call must start after them. If each call had its own copy of i, the right call would read the same tokens again. In Java she uses a class-level variable. In Python we use
self.i.→ Every call uses exactly one token, whether it's a value or a
#. Moving i right after reading guarantees the next call sees the next token. Forgetting to move it on the # path would loop on the same token.'#' in single quotes. In Java that's a char, not a String, so the comparison didn't work. She switched to double quotes. In Python both quote styles make strings, but compare strings with ==, never with is.5Approach steps (deserialize)
arr = data.split(","),i = 0.- build(): read
arr[i], theni += 1. - If the token is
"#"→ return None. - Otherwise make
TreeNode(int(token)), setleft = build(), thenright = build(). - Return the node. deserialize returns
build()of the first token.
6Code (Python)
class Codec:
def serialize(self, root):
if root is None:
return "#"
return str(root.val) + "," + self.serialize(root.left) + "," + self.serialize(root.right)
def deserialize(self, data):
self.arr = data.split(",") # "1,2,#" -> ["1", "2", "#"]
self.i = 0 # shared index into arr
return self.build()
def build(self):
val = self.arr[self.i] # take the next token
self.i += 1 # ...and move past it
if val == "#":
return None
root = TreeNode(int(val)) # "-7" -> -7
root.left = self.build() # next tokens describe the left subtree
root.right = self.build() # then the right subtree
return root7Code line by line
| line | what it means |
|---|---|
| self.arr = data.split(",") | Turn the string into a list of tokens so we can step through them by index. |
| self.i = 0 | Start at the first token. Stored on self so all calls share it. |
| val = self.arr[self.i] self.i += 1 | Take one token and move forward. Every call takes exactly one. |
| if val == "#": return None | The marker means "empty here". This is also the base case that stops the recursion. |
| root = TreeNode(int(val)) | A real value: make the node. int() handles negative numbers and several digits. |
| root.left = self.build() | The next tokens are the left subtree (preorder), so build it and store it. |
| root.right = self.build() | After the left subtree's tokens are used up, the right subtree's tokens come next. |
| return root | Give this finished subtree to the parent call. |
8Dry run with the index
- b() reads 1 (i → 1) → make node 1, build its left.
- b() reads 2 (i → 2) → make node 2, build its left.
- b() reads # (i → 3) → None. 2.left = None. Build 2's right.
- b() reads # (i → 4) → None. 2.right = None. Node 2 returns → 1.left = 2.
- Build 1's right: b() reads 3 (i → 5) → node 3, build its left.
- b() reads 4 (i → 6) → node 4. Its left reads # (i → 7), its right reads # (i → 8). 4 returns → 3.left = 4.
- 3's right: b() reads 5 (i → 9) → node 5. Its children read # (i → 10) and # (i → 11). 5 returns → 3.right = 5.
- 3 returns → 1.right = 3. 1 returns. The tree is rebuilt ✓, and i = 11, so exactly all tokens were used.
9Complexity & remember
- Time O(n): we read each token once and create each node once.
- Space: the recursion stack holds one call per level → O(height). But the token list from
splitholds about 2n + 1 strings → O(n). So overall it's O(n).
→ Python's default limit is about 1000 nested calls, so on your own machine a very tall tree can raise
RecursionError. Add import sys; sys.setrecursionlimit(20000) at the top if you test with huge skewed trees. The logic doesn't change.Part C · Revision page
| attempt | string for our tree | problem |
|---|---|---|
| values only | 12345 | can't tell 1,2 from 12 |
| values + commas | 1,2,3,4,5 | shape is lost (where are the empty spots?) |
| values + commas + # | 1,2,#,#,3,4,#,#,5,#,# | unique, round-trips |
| serialize | deserialize | |
|---|---|---|
| order | preorder: val, left, right | preorder: node, left, right |
| None | write # | read # → return None |
| shared state | none | one index self.i over the token list |
| time | O(n) | O(n) |
| space | O(n) string + O(h) stack | O(n) token list + O(h) stack |
class Codec:
def serialize(self, root):
out = []
def pre(node):
if node is None:
out.append("#")
return
out.append(str(node.val))
pre(node.left)
pre(node.right)
pre(root)
return ",".join(out)
def deserialize(self, data):
tokens = iter(data.split(",")) # next(tokens) plays the role of arr[i++]
def build():
val = next(tokens)
if val == "#":
return None
node = TreeNode(int(val))
node.left = build()
node.right = build()
return node
return build()2. A comma after each value, so multi-digit and negative values stay separate.
3. A # for every empty child, so the shape is saved.
4. Deserialize: split, use one shared index, take one token per call.
5. "#" → None, else node → left = build() → right = build(). O(n) time, O(n) space.
✗ no separator (12 vs 1,2)
✗ passing the index as a normal argument, so the right call re-reads the left's tokens
✗ forgetting to move the index forward on
#✗ using inorder (different trees can give the same string)
✗ comparing strings with
is instead of ==root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) root.right.left = TreeNode(4) root.right.right = TreeNode(5) c = Codec() s = c.serialize(root) print(s) # 1,2,#,#,3,4,#,#,5,#,# print(c.serialize(c.deserialize(s)) == s) # True print(c.deserialize(c.serialize(None))) # None
Based on this video: Serialize and Deserialize Binary Tree