Tree Construction and Serialization

intermediate tree serialization construction

Building a tree from scratch and converting it to/from a string are classic interview problems. They test whether we truly understand how traversals work — not just how to run them, but how they encode a tree’s structure.

Why We Need Two Traversals

A single traversal doesn’t uniquely define a tree. For example, the preorder [1, 2, 3] could be a left-skewed tree, a right-skewed tree, or a balanced tree. But if we have both preorder and inorder, the tree is uniquely determined.

The key insight: preorder tells us the root, inorder tells us what’s left and right of that root.

How Preorder + Inorder Defines a Tree
Preorder:
[3 9 20 15 7]
← first element is always the root
Inorder:
[9 3 15 20 7]
← left of root | root | right of root
Root = 3  |  Left subtree: [9]  |  Right subtree: [15, 20, 7]

Build Tree from Preorder + Inorder

The algorithm is recursive:

  1. The first element in preorder is the root.
  2. Find that root in inorder — everything to its left goes in the left subtree, everything to the right goes in the right subtree.
  3. Recurse on both halves.
function buildTree(preorder, inorder) {
  if (!preorder.length) return null;
  const root = new TreeNode(preorder[0]);
  const mid = inorder.indexOf(preorder[0]); // root's position in inorder
  root.left = buildTree(
    preorder.slice(1, mid + 1),    // left portion of preorder
    inorder.slice(0, mid)          // left portion of inorder
  );
  root.right = buildTree(
    preorder.slice(mid + 1),       // right portion of preorder
    inorder.slice(mid + 1)         // right portion of inorder
  );
  return root;
}
def build_tree(preorder, inorder):
    if not preorder: return None
    root = TreeNode(preorder[0])
    mid = inorder.index(preorder[0])  # root's position in inorder
    root.left = build_tree(
        preorder[1:mid + 1],    # left portion of preorder
        inorder[:mid]           # left portion of inorder
    )
    root.right = build_tree(
        preorder[mid + 1:],     # right portion of preorder
        inorder[mid + 1:]       # right portion of inorder
    )
    return root
int preIdx = 0;
Map<Integer, Integer> inMap = new HashMap<>();

TreeNode buildTree(int[] preorder, int[] inorder) {
    for (int i = 0; i < inorder.length; i++)
        inMap.put(inorder[i], i); // O(1) lookup for root position
    return build(preorder, 0, inorder.length - 1);
}
TreeNode build(int[] pre, int lo, int hi) {
    if (lo > hi) return null;
    TreeNode root = new TreeNode(pre[preIdx++]);
    int mid = inMap.get(root.val);
    root.left = build(pre, lo, mid - 1);
    root.right = build(pre, mid + 1, hi);
    return root;
}

Time: O(n) with a hash map for inorder lookups (O(n^2) without). Space: O(n).

The Java version is optimized — it uses a hash map to avoid the linear indexOf call and passes indices instead of creating new arrays.

Serialization: Tree to String

Serialization means converting a tree to a string so we can store it or send it over a network. Deserialization is the reverse — building the tree back from the string.

The simplest approach uses level-order (BFS) traversal. We include null markers for missing children so we know exactly where each node belongs.

Serialize: Tree → String
1
2
3
4
5
Serialized: "1,2,3,null,4,5,null"

Serialize (BFS Approach)

function serialize(root) {
  if (!root) return "";
  const result = [], queue = [root];
  while (queue.length) {
    const node = queue.shift();
    if (node) {
      result.push(node.val);
      queue.push(node.left);
      queue.push(node.right);
    } else {
      result.push("null");
    }
  }
  return result.join(",");
}
from collections import deque

def serialize(root):
    if not root: return ""
    result, queue = [], deque([root])
    while queue:
        node = queue.popleft()
        if node:
            result.append(str(node.val))
            queue.append(node.left)
            queue.append(node.right)
        else:
            result.append("null")
    return ",".join(result)
String serialize(TreeNode root) {
    if (root == null) return "";
    StringBuilder sb = new StringBuilder();
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        if (node != null) {
            sb.append(node.val).append(",");
            queue.add(node.left);
            queue.add(node.right);
        } else {
            sb.append("null,");
        }
    }
    return sb.toString();
}

Deserialize (BFS Approach)

We reverse the process: read values one by one, using a queue to assign children to each node in level order.

function deserialize(data) {
  if (!data) return null;
  const vals = data.split(",");
  const root = new TreeNode(parseInt(vals[0]));
  const queue = [root];
  let i = 1;
  while (queue.length && i < vals.length) {
    const node = queue.shift();
    if (vals[i] !== "null") {
      node.left = new TreeNode(parseInt(vals[i]));
      queue.push(node.left);
    }
    i++;
    if (vals[i] !== "null") {
      node.right = new TreeNode(parseInt(vals[i]));
      queue.push(node.right);
    }
    i++;
  }
  return root;
}
def deserialize(data):
    if not data: return None
    vals = data.split(",")
    root = TreeNode(int(vals[0]))
    queue, i = deque([root]), 1
    while queue and i < len(vals):
        node = queue.popleft()
        if vals[i] != "null":
            node.left = TreeNode(int(vals[i]))
            queue.append(node.left)
        i += 1
        if vals[i] != "null":
            node.right = TreeNode(int(vals[i]))
            queue.append(node.right)
        i += 1
    return root
TreeNode deserialize(String data) {
    if (data.isEmpty()) return null;
    String[] vals = data.split(",");
    TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    int i = 1;
    while (!queue.isEmpty() && i < vals.length) {
        TreeNode node = queue.poll();
        if (!vals[i].equals("null")) {
            node.left = new TreeNode(Integer.parseInt(vals[i]));
            queue.add(node.left);
        }
        i++;
        if (!vals[i].equals("null")) {
            node.right = new TreeNode(Integer.parseInt(vals[i]));
            queue.add(node.right);
        }
        i++;
    }
    return root;
}

Time: O(n) for both serialize and deserialize. Space: O(n) for the queue and output.

Which Traversal Pairs Work?

Not all pairs of traversals can uniquely reconstruct a tree:

PairWorks?Why
Preorder + InorderYesPreorder gives root, inorder splits left/right
Postorder + InorderYesPostorder gives root (last element), inorder splits
Preorder + PostorderOnly for full binary treesCan’t determine left vs right without inorder
Level-order + InorderYesLevel-order gives root, inorder splits

Key Takeaways

  • We need two traversals (and one must be inorder) to uniquely reconstruct a binary tree.
  • The recursive construction pattern: find root from preorder/postorder, split using inorder, recurse on both halves.
  • Serialization with null markers lets us reconstruct from a single traversal — because the nulls encode the structure.
  • BFS-based serialization is the most intuitive and commonly asked in interviews.

In simple language, constructing a tree is about figuring out “who’s the root?” and “what goes left vs right?” Serialization is just saving enough info to answer those questions later.