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 · 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.

given by LeetCode, don't write this in the solution
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

The three traversals (prerequisite)

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

Our example tree for the whole page:

      1
     / \
    2   3
       / \
      4   5

2What the constraints tell us

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.

Fix 1: a separatorPut a comma after every value: "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:

what we wanted
      1
     / \
    2   3
       / \
      4   5
what "1,2,3,4,5" rebuilds
          1
         /
        2
       /
      3
     /
    4
   /
  5

The 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.

Fix 2: a marker for NoneWhenever preorder reaches an empty child, write a special symbol instead of skipping it. The teacher uses #. 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:

string12##34##5##"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.

Doubt 1: she said she'd explain why preorder and not inorder or postorder. What's the reason?
→ 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)

  1. If the node is None → return "#".
  2. Otherwise return: the node's value, a comma, the serialized left subtree, a comma, the serialized right subtree.

6Code (Python)

Serialize with preorder (her approach)
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

linewhat 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.

  1. s(2) → "2," + s(None) + "," + s(None) = "2,#,#".
  2. s(4) → "4,#,#" and s(5) → "5,#,#" (both leaves).
  3. s(3) → "3," + "4,#,#" + "," + "5,#,#" = "3,4,#,#,5,#,#".
  4. s(1) → "1," + "2,#,#" + "," + "3,4,#,#,5,#,#" = "1,2,#,#,3,4,#,#,5,#,#" ✓
stack while writing 2's left
s(1)s(2)s(None) → "#"
stack at node 4
s(1)s(3)s(4)

9Complexity & remember

Doubt 2 (Python detail): is + 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.
Remember serializePreorder. None → "#". Otherwise val , left , right. Commas separate values, and # marks empty spots.

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

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:

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

Doubt 1: why must 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.
Doubt 2: why read the token and move i forward in one step?
→ 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.
Her slip in the editorHer Java code first failed because she wrote '#' 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)

  1. arr = data.split(","), i = 0.
  2. build(): read arr[i], then i += 1.
  3. If the token is "#" → return None.
  4. Otherwise make TreeNode(int(token)), set left = build(), then right = build().
  5. Return the node. deserialize returns build() of the first token.

6Code (Python)

Serialize + Deserialize (full Codec)
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 root

7Code line by line

linewhat it means
self.arr = data.split(",")Turn the string into a list of tokens so we can step through them by index.
self.i = 0Start at the first token. Stored on self so all calls share it.
val = self.arr[self.i] self.i += 1Take one token and move forward. Every call takes exactly one.
if val == "#": return NoneThe 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 rootGive this finished subtree to the parent call.

8Dry run with the index

index012345678910
token12##34##5##
  1. b() reads 1 (i → 1) → make node 1, build its left.
  2. b() reads 2 (i → 2) → make node 2, build its left.
  3. b() reads # (i → 3) → None. 2.left = None. Build 2's right.
  4. b() reads # (i → 4) → None. 2.right = None. Node 2 returns → 1.left = 2.
  5. Build 1's right: b() reads 3 (i → 5) → node 3, build its left.
  6. b() reads 4 (i → 6) → node 4. Its left reads # (i → 7), its right reads # (i → 8). 4 returns → 3.left = 4.
  7. 3's right: b() reads 5 (i → 9) → node 5. Its children read # (i → 10) and # (i → 11). 5 returns → 3.right = 5.
  8. 3 returns → 1.right = 3. 1 returns. The tree is rebuilt ✓, and i = 11, so exactly all tokens were used.
stack at step 3
b → 1b → 2b → # (None)
stack at step 6
b → 1b → 3b → 4

9Complexity & remember

Doubt (Python detail): a skewed tree with 10⁴ nodes is 10⁴ levels deep. Will Python's recursion survive that?
→ 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.
Remember deserializesplit(",") → one shared index → each call takes one token: "#" → None, else node, then left = build(), right = build().

Part C · Revision page

attemptstring for our treeproblem
values only12345can't tell 1,2 from 12
values + commas1,2,3,4,5shape is lost (where are the empty spots?)
values + commas + #1,2,#,#,3,4,#,#,5,#,#unique, round-trips
serializedeserialize
orderpreorder: val, left, rightpreorder: node, left, right
Nonewrite #read # → return None
shared statenoneone index self.i over the token list
timeO(n)O(n)
spaceO(n) string + O(h) stackO(n) token list + O(h) stack
same idea, Python-friendly: build a list and join once
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()
If you remember only 5 lines 1. Serialize with preorder, because the root comes first and it's easy to replay.
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.
Mistakes to avoid ✗ skipping None children while serializing (the shape is lost)
✗ 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 ==
test it yourself (paste under either Codec)
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