Skip to content

297. Serialize and Deserialize Binary Tree (Hard)

Problem

Design an algorithm to serialize a binary tree to a string and deserialize that string back to the original tree. There’s no required format, just that serialize/deserialize are inverses.

Example

  • root = [1,2,3,null,null,4,5] → some string, then back to [1,2,3,null,null,4,5]

LeetCode 297 · Link · Hard

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: BFS with null markers

Serialize level-by-level, using a sentinel ("#") for missing children. Deserialize by consuming the tokens in order and wiring up children with a queue.

from collections import deque
class Codec:
def serialize(self, root):
if not root:
return ""
parts = []
q = deque([root]) # L1: O(1) init
while q:
node = q.popleft() # L2: O(1) dequeue
if node:
parts.append(str(node.val)) # L3: O(1) append value
q.append(node.left) # L4: enqueue left (may be None)
q.append(node.right) # L5: enqueue right (may be None)
else:
parts.append("#") # L6: O(1) append null marker
return ",".join(parts) # L7: O(n) join
def deserialize(self, data):
if not data:
return None
tokens = data.split(",") # L8: O(n) split
root = TreeNode(int(tokens[0]))
q = deque([root])
i = 1
while q and i < len(tokens):
node = q.popleft() # L9: O(1) dequeue
if tokens[i] != "#":
node.left = TreeNode(int(tokens[i]))
q.append(node.left) # L10: O(1) wire left
i += 1
if i < len(tokens) and tokens[i] != "#":
node.right = TreeNode(int(tokens[i]))
q.append(node.right) # L11: O(1) wire right
i += 1
return root

Where the time goes, line by line

Variables: n = number of nodes in the tree, h = tree height, w = max tree width.

LinePer-call costTimes executedContribution
L2/L3/L4/L5/L6 (per token)O(1)O(1)2n+1 tokensO(n)O(n)
L7 (join)O(n)O(n)1O(n)O(n) ← dominates (all lines tie)
L8 (split)O(n)O(n)1O(n)O(n)
L9/L10/L11 (deserialize)O(1)O(1)nO(n)O(n)

Every node produces exactly one value token and both missing children produce # tokens. A tree with n nodes has 2n+1 tokens total (n values + n+1 nulls).

Complexity

  • Time: O(n)O(n) for both serialize and deserialize, driven by L7/L8.
  • Space: O(n)O(n) output + O(w)O(w) queue.

The LeetCode’s own serialization format uses this approach; it’s the easiest to debug visually.

Approach 2: Preorder DFS with null markers (often cleanest)

Recursive serialize in preorder, emitting "#" for missing children. Deserialize recursively consuming tokens from an iterator.

class Codec:
def serialize(self, root):
parts = []
def rec(node):
if not node:
parts.append("#") # L1: O(1) emit null
return
parts.append(str(node.val)) # L2: O(1) emit value
rec(node.left) # L3: recurse left
rec(node.right) # L4: recurse right
rec(root)
return ",".join(parts) # L5: O(n) join
def deserialize(self, data):
tokens = iter(data.split(",")) # L6: O(n) split + iterator
def rec():
val = next(tokens)
if val == "#":
return None # L7: null marker
node = TreeNode(int(val))
node.left = rec() # L8: recurse left
node.right = rec() # L9: recurse right
return node
return rec()

Where the time goes, line by line

Variables: n = number of nodes in the tree, h = tree height.

LinePer-call costTimes executedContribution
L1/L2 (emit)O(1)O(1)2n+1O(n)O(n)
L3/L4 (recurse)O(1)O(1) dispatchnO(n)O(n)
L5 (join)O(n)O(n)1O(n)O(n) ← dominates (all lines tie)
L6 (split)O(n)O(n)1O(n)O(n)
L8/L9 (recurse)O(1)O(1) dispatchnO(n)O(n)

Complexity

  • Time: O(n)O(n).
  • Space: O(n)O(n) output + O(h)O(h) recursion.

Shortest correct answer and arguably the cleanest.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Postorder DFS with null markers

Symmetric to preorder but emits children before the parent. Deserialize by consuming tokens right-to-left.

class Codec:
def serialize(self, root):
parts = []
def rec(node):
if not node:
parts.append("#")
return
rec(node.left) # L1: recurse left first
rec(node.right) # L2: recurse right
parts.append(str(node.val)) # L3: emit parent last
rec(root)
return ",".join(parts)
def deserialize(self, data):
tokens = data.split(",")
def rec():
val = tokens.pop() # L4: consume right-to-left
if val == "#":
return None
node = TreeNode(int(val))
# NB: right child is deserialized before left because we pop from the end
node.right = rec() # L5: right before left
node.left = rec()
return node
return rec()

Where the time goes, line by line

Variables: n = number of nodes in the tree, h = tree height.

LinePer-call costTimes executedContribution
L1/L2/L3 (recurse + emit)O(1)O(1)nO(n)O(n)
L4 (pop)O(1)O(1)2n+1O(n)O(n) ← dominates (all lines tie)
L5 (recurse)O(1)O(1) dispatchnO(n)O(n)

Complexity

  • Time: O(n)O(n).
  • Space: O(n)O(n) + O(h)O(h).

Less common in practice; included to show the symmetry.

Summary

ApproachTimeSpaceNotes
BFS with null markersO(n)O(n)O(n)O(n) + O(w)O(w)Matches LeetCode’s own display format
Preorder DFSO(n)O(n)O(n)O(n) + O(h)O(h)Shortest and cleanest
Postorder DFSO(n)O(n)O(n)O(n) + O(h)O(h)Symmetric curiosity

Preorder DFS is the interview-favorite. BFS is easier to eyeball-debug.

Aside: size of the serialization

With null markers, every internal node contributes 1 value token and every leaf contributes 1 value + 2 nulls. A tree with n nodes has n + 1 null slots (the external “holes”), giving a total of 2n + 1 tokens, linear in n.

Test cases

class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(vals):
if not vals: return None
from collections import deque
root = TreeNode(vals[0])
q = [root]
i = 1
while q and i < len(vals):
node = q.pop(0)
if i < len(vals) and vals[i] is not None:
node.left = TreeNode(vals[i])
q.append(node.left)
i += 1
if i < len(vals) and vals[i] is not None:
node.right = TreeNode(vals[i])
q.append(node.right)
i += 1
return root
def tree_to_list(root):
from collections import deque
if not root: return []
result, q = [], deque([root])
while q:
node = q.popleft()
if node:
result.append(node.val)
q.append(node.left)
q.append(node.right)
else:
result.append(None)
while result and result[-1] is None:
result.pop()
return result
# Using Preorder DFS Codec
class Codec:
def serialize(self, root):
parts = []
def rec(node):
if not node:
parts.append("#")
return
parts.append(str(node.val))
rec(node.left)
rec(node.right)
rec(root)
return ",".join(parts)
def deserialize(self, data):
tokens = iter(data.split(","))
def rec():
val = next(tokens)
if val == "#":
return None
node = TreeNode(int(val))
node.left = rec()
node.right = rec()
return node
return rec()
def _run_tests():
codec = Codec()
# example from problem
t = build_tree([1, 2, 3, None, None, 4, 5])
assert tree_to_list(codec.deserialize(codec.serialize(t))) == [1, 2, 3, None, None, 4, 5]
# empty tree
assert codec.deserialize(codec.serialize(None)) is None
# single node
t2 = build_tree([42])
assert codec.deserialize(codec.serialize(t2)).val == 42
# left-skewed
t3 = build_tree([1, 2, None, 3])
assert tree_to_list(codec.deserialize(codec.serialize(t3))) == [1, 2, None, 3]
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Tree Traversal, the recursive or iterative visit pattern for carrying path and subtree state.
  • DFS, the depth first traversal habit of following one branch before returning.