Skip to content
← All posts

Construct BST from Preorder Traversal: Upper Bound Recursion

6 min read
leetcodeproblemmediumarraystreesbinary-search

You are given an array of integers representing the preorder traversal of a binary search tree. Your task is to reconstruct the original BST and return its root. This is LeetCode 1008: Construct Binary Search Tree from Preorder Traversal.

Example: preorder = [8, 5, 1, 7, 10, 12] produces a BST with root 8, left subtree containing 5, 1, 7, and right subtree containing 10, 12.

pre8root517101285101712
Preorder [8, 5, 1, 7, 10, 12] reconstructed into the BST. Each value is placed by comparing against the current node and going left or right.

Why this problem matters

Constructing a BST from a single traversal is a foundational skill that tests whether you really understand two things at once: how preorder traversal works and how BST ordering constraints shape the tree. Unlike building a generic binary tree (which requires two traversal arrays), a BST can be rebuilt from preorder alone because the BST property tells you exactly where each value belongs.

This problem also introduces the upper bound recursion technique, a pattern that shows up in BST validation, serialization, and other tree construction problems.

The key insight

In a preorder traversal, the first element is always the root. Everything after it that is smaller belongs to the left subtree, and everything larger belongs to the right subtree. You could scan the array to find where the split happens, but that would cost O(n) per level and O(n log n) or O(n^2) overall.

The faster approach is to maintain an index into the preorder array and an upper bound. You walk through the array in order, and for each recursive call, you pass a bound that says "values in this subtree must be less than this bound." When the current value exceeds the bound, you know you have left this subtree, so you return None and let the parent handle it.

Here is the algorithm:

  1. Start with index 0 and an upper bound of infinity.
  2. If the index is out of bounds or the current value exceeds the upper bound, return None.
  3. Create a node with the current value and advance the index.
  4. Recurse left with the current node's value as the new upper bound.
  5. Recurse right with the parent's upper bound (passed down).
  6. Return the node.

The solution

def bst_from_preorder(preorder):
    i = 0

    def build(bound):
        nonlocal i
        if i == len(preorder) or preorder[i] > bound:
            return None
        val = preorder[i]
        i += 1
        node = TreeNode(val)
        node.left = build(val)
        node.right = build(bound)
        return node

    return build(float("inf"))

The bound parameter is the secret to making this O(n). Each call to build either creates a node and advances i, or immediately returns None. Since i only moves forward and there are n values, the total work across all calls is O(n).

When you call build(val) for the left child, you are saying "only accept values less than val." When you call build(bound) for the right child, you are saying "accept values up to the bound that was set by the ancestor above." This is how the BST ordering constraint is enforced at every level of the recursion.

The upper bound technique works because BST ordering gives you a natural way to decide when a subtree is "done." You do not need to scan ahead in the array to find partition points. The bound tells you when to stop, and the recursion handles the rest.

Visual walkthrough

Step 1: Insert 8 as the root

8

The first element in preorder is always the root of the BST.

Step 2: Insert 5. Since 5 < 8, it goes to the left of 8.

85

5 is less than the upper bound (8), so it becomes the left child of 8.

Step 3: Insert 1. Since 1 < 5, it goes to the left of 5.

851

1 is less than 5 and less than 8. It becomes the left child of 5.

Step 4: Insert 7. Since 7 > 5 but 7 < 8, it goes to the right of 5.

8517

7 exceeds the bound for 5's left subtree but stays within 8's left subtree. It becomes 5's right child.

Step 5: Insert 10. Since 10 > 8, it goes to the right of 8.

851017

10 exceeds the upper bound for 8's left subtree. The recursion returns to 8, and 10 becomes its right child.

Step 6: Insert 12. Since 12 > 10, it goes to the right of 10. Done!

85101712

12 exceeds 10 but has no upper bound restriction. It becomes 10's right child. The BST is complete.

Notice how each value is placed exactly once, in the order it appears in the preorder array. The bound mechanism ensures that values "bounce back" to the correct parent when they do not fit in the current subtree.

Complexity analysis

ApproachTimeSpace
Upper bound recursionO(n)O(n)

Time is O(n). Each element in the preorder array is visited exactly once. The index i advances from 0 to n across all recursive calls, and each call does O(1) work.

Space is O(n). The recursion stack can go as deep as the height of the tree. In the worst case (a sorted ascending array producing a right-skewed tree, or sorted descending producing a left-skewed tree), the height is n. For a balanced BST, the stack depth is O(log n). The tree itself also uses O(n) space for the n nodes.

The building blocks

1. Preorder traversal property

Preorder visits root, then left subtree, then right subtree. This means the first element is always the root, and the remaining elements naturally divide into a left group (smaller values) followed by a right group (larger values). You do not need a second traversal array to find the split because the BST property provides that information.

2. Upper bound recursion pattern

The bound parameter acts as a gate. Each recursive call says "I will only consume values that fit within my BST constraint." When a value exceeds the bound, the call returns None, effectively saying "this value does not belong to me." The parent then gets a chance to use that value for its right subtree. This pattern avoids scanning the array for partition points and keeps the algorithm linear.

def build(bound):
    nonlocal i
    if i == len(preorder) or preorder[i] > bound:
        return None
    val = preorder[i]
    i += 1
    node = TreeNode(val)
    node.left = build(val)
    node.right = build(bound)
    return node

This same bound-passing pattern appears in BST validation (LeetCode 98), where you check that every node's value falls within a valid range defined by its ancestors.

Edge cases

  • Single node: preorder = [1]. The root is created, both recursive calls return None immediately, and you get a one-node tree.
  • Sorted ascending (e.g., [1, 2, 3, 4]): every value goes to the right, producing a right-skewed tree. Recursion depth is O(n).
  • Sorted descending (e.g., [4, 3, 2, 1]): every value goes to the left, producing a left-skewed tree. Recursion depth is O(n).
  • Two elements: [5, 3] gives root 5 with left child 3. [5, 8] gives root 5 with right child 8.
  • All values equal: the problem guarantees distinct values, so this case does not arise. If it did, the BST property (strictly less on the left) would need clarification.

From understanding to recall

You now understand how the upper bound recursion technique builds a BST from preorder in linear time. The idea is simple once you see it: pass a bound, consume values that fit, and return when they do not. But the details matter. You need to remember that the left child gets the current node's value as its bound, the right child inherits the parent's bound, and the index advances globally across all calls.

These details are easy to mix up under pressure. Did the left child get val or bound? Does the index reset between calls? Spaced repetition helps you internalize these specifics so they become automatic. After a few reps, the pattern clicks and you can write it without hesitation.

The upper bound technique also transfers directly to BST validation (LeetCode 98) and BST serialization problems. Drilling this problem builds a foundation for all of them.

Related posts

CodeBricks helps you move from understanding a solution to recalling it under pressure. Instead of re-reading explanations, you practice writing the code from scratch at spaced intervals until the pattern is automatic.