leetcode / Trees / Building and serializing trees advanced

Building and serializing trees

Walking a tree turns it into a list. Building a tree is the reverse: given the list, recover the shape. A single traversal is usually not enough, because several different trees produce the same list, but two traversals (or one traversal plus markers for the missing children) pin the tree down exactly. Every construction problem is the same recursion: take the root from the front of one list, use the other list to find where the left side ends, and recurse on the two pieces.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. Preorder plus inorder
  5. Serialization with markers
  6. Balanced trees from sorted input
  7. Worked example
  8. Implementation
  9. Variations and edge cases
  10. Complexity and tradeoffs
  11. Problems
  12. Easy
  13. Medium
  14. Hard
  15. Also uses this pattern
  16. Connections
  17. Read first
  18. Read next
  19. Check yourself
  20. References

Problem

You are handed the output of a traversal and asked for the tree, or asked to write a tree to a string and read it back. Three ideas cover the whole family. Reconstruct from two orders: preorder tells you the root first, inorder tells you how many nodes sit on its left. Serialize with markers: write a placeholder for each missing child so that one preorder or level-order list is unambiguous. Build a balanced tree from sorted input: choose the middle as the root so the two halves have nearly equal size. The maximum-path-sum question that is sometimes grouped here is solved on the tree depth-first search page, because it computes values on an existing tree rather than building one.

SignalPatternWhy
"Build the tree from preorder and inorder (or inorder and postorder)"Root from one list, split by the otherThe position of the root in the inorder list says how many nodes belong to the left subtree.
"Only one traversal is given" and the tree is a search treeUse value bounds instead of the second listThe ordering invariant supplies what the missing traversal would have.
"Serialize and deserialize"Preorder with null markers, or level order with null markersThe markers make the shape explicit, so one list is enough.
"Convert a sorted array or list into a balanced tree"Middle element is the root, recurse on halvesEqual halves keep the height at about log n.
"Many lookups of root position in the inorder list"Dictionary from value to indexA list search on every call would cost O(n) each, O(n²) overall.
"Values may repeat"Ask for the rule; two traversals may not determine the treeThe root's position in the inorder list is ambiguous when values repeat.
"Encode a tree compactly" or "copy a tree"Same recursion, different output typeSerialization, cloning and construction all follow the node, left, right shape of the tree.

Intuition

Preorder is "root, then the entire left subtree, then the entire right subtree". So the first value is always the root, and after it come all the left-subtree values as one contiguous block, followed by the right-subtree block. What preorder does not say is where the left block ends. Inorder is "left subtree, root, right subtree", so once you find the root inside the inorder list, everything to its left is the left subtree and everything to its right is the right subtree. The inorder list supplies the size of the left block; preorder supplies the identity of each root. Together they determine the tree when the values are distinct.

Why one traversal is not enough: the preorder list 1, 2 is produced by both "1 with left child 2" and "1 with right child 2". The shape information is missing. Serialization fixes this differently: it records the missing children explicitly, so the list itself says exactly where each subtree stops. With markers, the list has a self-delimiting structure: after reading a node you recursively read its left subtree (which ends with its own markers) and then its right subtree, and you always know when you are done.

Splitting inorder around the first preorder value Two rows of boxes. The top row is the preorder list 3, 9, 20, 15, 7; the first value, 3, is the root. The bottom row is the inorder list 9, 3, 15, 20, 7. The root 3 sits at index 1, so the 1 value to its left, 9, form the left subtree and the 3 values to its right, 15, 20, 7, form the right subtree. preorder inorder 3 9 20 15 7 9 3 15 20 7 root

Amber is the root. Green is the left subtree's inorder block and blue is the right subtree's. The left block has 1 value, so the next 1 value after the root in preorder are the left subtree's own preorder.

Mechanics

Preorder plus inorder

Keep a cursor into the preorder list. The function build(lo, hi) builds the subtree whose inorder values occupy inorder[lo..hi]. If the range is empty, return None. Otherwise take preorder[cursor] as the root, advance the cursor, look up the root's index mid in inorder, build the left subtree from (lo, mid-1) and then the right from (mid+1, hi). The order matters: the cursor must finish consuming the left block before the right subtree reads its root, which is exactly the order preorder lists them. With a dictionary from value to inorder index, each call is O(1) plus recursion, so construction is O(n).

Correctness is induction on the range size. For an empty range the answer is empty. For a non-empty range, the next preorder value is the root of this subtree (preorder starts with the root), the inorder range splits at the root's index into the left and right value sets, and the recursive calls consume exactly those many preorder values, so the cursor lands on the right block for each. With inorder and postorder instead, the root is the last unread postorder value, so you read from the back and build the right subtree first.

Serialization with markers

Write a preorder walk that emits the value for a node and a marker (say #) for each missing child. A tree with n nodes has n+1 missing children, so the output has 2n+1 tokens. To read it back, consume tokens recursively: a marker returns None; a value creates a node whose left and right come from the next two recursive reads. Because the walk is deterministic, reading mirrors writing exactly, and the round trip returns the original tree. A level-order variant works the same way with a queue and is how many systems print trees; it is easier to read by eye and no harder to implement.

Balanced trees from sorted input

Given sorted values, any choice of root splits the array into a left part and a right part, and the inorder property of a search tree already holds. The only question is height. Choosing the middle makes the two parts differ in size by at most one, so the height is the ceiling of log₂(n+1). Choosing the first element would give a chain of height n. For a sorted linked list the same idea applies; find the middle with fast and slow pointers, or build bottom-up while consuming the list in order.

A tree and its preorder serialization with markers A tree with 5 nodes and root 3. Below it is the serialized form, in order: 3, 9, #, #, 20, 15, #, #, 7, #, #. Each hash marks a missing child, and there are 6 of them. 3 9 20 15 7 3 9 # # 20 15 # # 7 # #

The tree above and its serialization below: green boxes are values, blue boxes are markers for missing children. The strip scrolls sideways on a narrow screen.

Worked example

Rebuilding. Preorder 3, 9, 20, 15, 7, inorder 9, 3, 15, 20, 7. Each row is one call, in the order the calls happen:

CallRoot (next in preorder)Left inorder blockRight inorder block
13915, 20, 7
29emptyempty
320157
415emptyempty
57emptyempty

The first root is 3; it sits at inorder index 1, so 9 goes left and 15, 20, 7 goes right. The left block has one value, so the very next preorder value (9) is the entire left subtree. The right block then starts with the next preorder value, 20. The tree has root 3, left child 9 and right child 20, which has children 15 and 7.

Serializing. The preorder walk with markers produces 3 9 # # 20 15 # # 7 # #. The serialized string has 11 tokens for 5 nodes: 2n + 1, because every node contributes itself and each of the n + 1 empty child slots contributes a marker. Reading it back gives the same tree.

Balanced from sorted. For the sorted array -10, -3, 0, 5, 9: the middle element 0 becomes the root, so -10, -3 goes left and 5, 9 goes right. In the left half the middle (index 1 of two) is -3 with -10 to its left; in the right half it is 9 with 5 to its left. The height is 3, the minimum possible for 5 nodes.

Implementation

from collections import deque


class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right


def build_tree(preorder, inorder):
    index = {val: i for i, val in enumerate(inorder)}   # O(1) root lookup
    cursor = 0

    def build(lo, hi):
        nonlocal cursor
        if lo > hi:
            return None
        root = TreeNode(preorder[cursor])
        cursor += 1
        mid = index[root.val]
        root.left = build(lo, mid - 1)                   # must run before the right side
        root.right = build(mid + 1, hi)
        return root

    return build(0, len(inorder) - 1)


def build_tree_post(inorder, postorder):
    index = {val: i for i, val in enumerate(inorder)}
    cursor = len(postorder) - 1

    def build(lo, hi):
        nonlocal cursor
        if lo > hi:
            return None
        root = TreeNode(postorder[cursor])
        cursor -= 1
        mid = index[root.val]
        root.right = build(mid + 1, hi)                  # right first when reading from the back
        root.left = build(lo, mid - 1)
        return root

    return build(0, len(inorder) - 1)


def serialize(root):
    out = []

    def walk(node):
        if not node:
            out.append("#")
            return
        out.append(str(node.val))
        walk(node.left)
        walk(node.right)

    walk(root)
    return ",".join(out)


def deserialize(data):
    tokens = iter(data.split(","))

    def build():
        token = next(tokens)
        if token == "#":
            return None
        node = TreeNode(int(token))
        node.left = build()
        node.right = build()
        return node

    return build()


def serialize_levels(root):
    out, queue = [], deque([root])
    while queue:
        node = queue.popleft()
        if node:
            out.append(str(node.val))
            queue.append(node.left)
            queue.append(node.right)
        else:
            out.append("#")
    return ",".join(out)


def deserialize_levels(data):
    tokens = data.split(",")
    if tokens[0] == "#":
        return None
    root = TreeNode(int(tokens[0]))
    queue, i = deque([root]), 1
    while queue:
        node = queue.popleft()
        if tokens[i] != "#":
            node.left = TreeNode(int(tokens[i]))
            queue.append(node.left)
        i += 1
        if tokens[i] != "#":
            node.right = TreeNode(int(tokens[i]))
            queue.append(node.right)
        i += 1
    return root


def sorted_array_to_bst(nums):
    def build(lo, hi):                                   # half-open [lo, hi)
        if lo >= hi:
            return None
        mid = (lo + hi) // 2
        return TreeNode(nums[mid], build(lo, mid), build(mid + 1, hi))

    return build(0, len(nums))

build_tree never slices the lists, so it is O(n) rather than the O(n²) of the version that copies inorder[:mid] at every call. deserialize_levels relies on the string being well-formed, with a marker for every missing child of every real node, which is what serialize_levels writes.

Variations and edge cases

  • Inorder plus postorder. Read the postorder list from the back and build the right subtree before the left, as above. Preorder plus postorder alone is ambiguous for nodes with a single child.
  • Search tree from preorder alone. Pass an upper bound down and consume values while they are below it; the ordering invariant replaces the inorder list, giving O(n) time with no map.
  • Compact serialization. Drop the trailing markers of a level-order string, use a delimiter that cannot appear in values, and encode negative numbers safely. A search tree can be serialized with no markers at all, as plain preorder.
  • Sorted linked list to tree. Count the length first, then build the left half recursively while advancing a shared list pointer, so the inorder order matches the list order.
  • Edge cases: an empty tree (serialize to a lone marker), a single node, a chain (the recursion depth is n, so mind Python's limit), negative or multi-digit values (split on a delimiter, never on characters), and duplicated values, where the dictionary lookup is no longer valid.

Complexity and tradeoffs

  • Time: O(n) for every routine here with a value-to-index dictionary; without it, finding the root in the inorder list makes the build O(n²). Serialization and deserialization are O(n) with 11 tokens for the 5-node example.
  • Space: O(n) for the dictionary and the output, plus O(h) recursion depth. The level-order version uses a queue of up to the widest level.
  • Baseline. Slicing the arrays at every call gives the same answer but copies O(n) values per call, O(n²) in the worst case. Index bounds and a cursor remove the copying.
  • When it fails. Repeated values make two traversals ambiguous, and a single traversal without markers never determines the shape. Very deep trees overflow the recursion limit, so either raise it carefully or convert to the level-order versions, which use a queue and not the call stack.

Problems

The three home problems are the three ideas: rebuild from two traversals, serialize with markers and turn sorted input into a balanced tree. Quad-tree construction is a close cousin with four children; it appears below as "also uses".

Easy

Medium

Hard

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Read first

Read next

No page builds on this one yet.

This page runs the traversals of the depth-first page backwards and uses the breadth-first page's queue for the level-order encoding. The balanced build is the binary search tree page's invariant used constructively. The same "split by a known position and recurse on both sides" shape appears in divide and conquer, merge sort and segment tree construction later in the roadmap.

Check yourself

Why must the left subtree be built before the right in the preorder-plus-inorder build?

The preorder cursor is shared. Preorder lists the whole left block before the right block, so the left call has to consume its values first; building the right first would make it read the left subtree's root. In the example the left block 9 consumes one value, and the right call then starts at 20.

Why does a tree with n nodes serialize to exactly 2n + 1 tokens?

Each of the n nodes writes one value token. A binary tree with n nodes has 2n child slots, of which n - 1 are filled by nodes (every node except the root is someone's child), leaving n + 1 empty slots, each written as a marker. In total n + n + 1 = 2n + 1. Here, 5 nodes give 11 tokens.

Why is preorder alone not enough to rebuild a binary tree, but preorder with markers is?

Without markers the list does not say where the left subtree stops: 1, 2 could be a left child or a right child. Markers record the missing children, so every subtree is self-delimiting and the reader always knows when the left side is finished.

Choosing the middle element as the root gives height 3 for 5 values. What would choosing the first element give?

A right-leaning chain of height 5, since every later value is larger and goes to the right. Search then costs O(n), no better than scanning the array.

References

  • Knuth, The Art of Computer Programming, volume 1, section 2.3.1: binary tree traversals and the exercises on reconstructing a tree from two orders.
  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, section 12.1 (tree walks) and the exercises on building a tree from a traversal.
  • Sedgewick and Wayne, Algorithms, section 3.2: level-order traversal and tree construction.
  • Skiena, The Algorithm Design Manual, the divide-and-conquer chapter, for the "split at a known position" argument.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.