DSA sheet · Trees · Binary Search Tree pattern

BST from Preorder

In the binary tree section we rebuilt a tree from two traversals (preorder + inorder, and so on). A plain binary tree needs two. A BST needs only one, because the BST rule tells us where every value must go. The teacher solves it twice: first a brute force that searches for the "split point" at every node (O(n²)), then an optimal one that carries an upper bound down the recursion and builds the tree in a single pass (O(n)).

The upper-bound trick is worth learning well. It's the same "allowed range" thinking as Validate BST, but used to build a tree instead of checking one.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a tree node?

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

The BST property

In a Binary Search Tree, for every node: every value in its left subtree (the left child and everything below it) is smaller than the node, and every value in its right subtree is bigger. It's about all nodes below, not just the children.

BST ✓
      8
     / \
    5   10
   / \
  1   7
NOT a BST ✗
      8
     / \
    5   10
   / \
  1   9

9 is bigger than its parent 5, so as a right child of 5 it looks fine. But it's in the left subtree of 8, and 9 > 8. That breaks the rule.

A side fact you'll see often: an inorder walk (left → node → right) of a BST gives the values in sorted order. At each node it lists all smaller values, then the node, then all bigger ones. This problem doesn't need it, but it explains why one traversal is enough: the inorder of a BST is always just its values sorted.

Preorder traversal

Preorder visits node → left subtree → right subtree, doing the same thing inside each subtree. For the BST above (with 12 as 10's right child, see Example 1 below) preorder gives 8 5 1 7 10 12.

preorder:  [ 8 | 5  1  7 | 10  12 ]
            root  left (<8)  right (>8)

Part A · Brute force: find the split point

LeetCode 1008

1The question in simple words

You get a list preorder, which is the preorder traversal of some BST. You aren't given the tree. Build that BST and return its root.

Example 1: [8, 5, 1, 7, 10, 12]
        8
       / \
      5   10
     / \    \
    1   7    12
Example 2: [1, 3]
    1
     \
      3

2What the constraints tell us

3Intuition

The first value is the root. Everything after it splits into two blocks: first the values smaller than the root (left subtree), then the rest (right subtree). So: make the root, find where the smaller block ends, and build the left subtree from the first block and the right subtree from the second, using the same function. Each block is itself a preorder of a smaller BST, so the same rule applies inside it.

4Building the logic from the example

Where does the left block end?

We can't know the left block's size in advance. Maybe only 5 is on the left and everything else is on the right. Maybe everything is on the left and the right is empty. The teacher's way to find it: use the definition. Left-subtree values are smaller than the root. So start right after the root and move forward while the values are smaller than 8:

So we pass the whole preorder list plus two indexes (start, end) that say which part to build from. We don't make new copies of the list.

The base case is on the indexes, not on a node

Usually the base case is "if root is None". Here there's no tree yet. We are creating nodes from index ranges, so the stop condition must be about the indexes:

Don't run off the end of the range while scanning

What if every remaining value is smaller than the root (everything goes left, the right is empty)? Then the "smaller than root" check never fails and i would walk out of the range. So the loop must also check that i is still inside the range.

Doubt (fix in these notes): in the video the loop says while i < end and …. Is that right?
→ It's off by one, because end is the last valid index (inclusive), not one past it. Try [3, 2, 1]: at the root 3, i goes to 1 (2 < 3), then stops at i = 2 because 2 < 2 is false. It never looks at the 1. Then the left block is [2] and the right block is [1], so 1 becomes the right child of 3, which is wrong. With i <= end the scan checks the last value too, i stops at 3, the left block is [2, 1] and the right block is empty ✓. (On the teacher's own example the last value, 12, is bigger than the root, so the bug doesn't show.)

Always return the root you made

Building works on the way back up. When we create 5, it isn't attached to 8 yet. It's attached only when its call returns it and the parent stores it in root.left. The same goes for 1 and 7 hanging under 5.

Doubt: what happens if I forget return root?
→ A Python function with no return gives back None. So 5's call would hand None to 8, and 8.left = None. The node 5 (and 1, 7 under it) would still exist in memory but be cut off, not part of the tree. That's why every build call must end with return root.

5Approach steps

  1. build(start, end): if start > end → return None.
  2. Make root from preorder[start].
  3. Scan i from start + 1 while i <= end and preorder[i] < root.val.
  4. root.left = build(start + 1, i − 1), root.right = build(i, end).
  5. Return root. Start with build(0, n − 1).

6Code (Python)

BST from Preorder, brute force (split point)
class Solution:
    def bstFromPreorder(self, preorder):
        return self.build(preorder, 0, len(preorder) - 1)

    def build(self, preorder, start, end):
        if start > end:                     # empty range
            return None
        root = TreeNode(preorder[start])     # first value = root
        i = start + 1
        while i <= end and preorder[i] < root.val:   # <= (fixed)
            i += 1
        root.left = self.build(preorder, start + 1, i - 1)
        root.right = self.build(preorder, i, end)
        return root                          # this is what connects it

7Code line by line

linewhat it means
self.build(preorder, 0, len(preorder) - 1)Build from the whole list. end is the last valid index.
if start > end: return NoneThe range is empty, so there's no node in this spot. Index-based base case.
root = TreeNode(preorder[start])In preorder, the first value of any block is that block's root.
i = start + 1Start scanning right after the root.
while i <= end and preorder[i] < root.valWalk over the left block (values smaller than the root). Check the range first, so we never read past end.
root.left = build(start + 1, i - 1)The smaller block becomes the left subtree.
root.right = build(i, end)Everything from the first bigger value onwards becomes the right subtree.
return rootHand the finished subtree to the parent so it gets attached.

8Dry run (hand table)

preorder = [8, 5, 1, 7, 10, 12], indexes 0–5.

call (start, end)rootscan stops at ileft callright call
(0, 5)84 (value 10)(1, 3) → [5, 1, 7](4, 5) → [10, 12]
(1, 3)53 (value 7)(2, 2) → [1](3, 3) → [7]
(2, 2)13 (past end)(3, 2) → None(3, 2) → None
(3, 3)74 (past end)(4, 3) → None(4, 3) → None
(4, 5)105 (value 12)(5, 4) → None(5, 5) → [12]
(5, 5)126 (past end)(6, 5) → None(6, 5) → None

Connections happen on the way back: 1 and 7 attach to 5 when their calls return, then 5 attaches to 8. 12 attaches to 10, then 10 attaches to 8. Result: Example 1's tree ✓

stack while building 1
build(0,5) → 8build(1,3) → 5build(2,2) → 1

9Complexity & remember

Remember brute forceRoot = first value. Scan forward while smaller (i <= end!). Left = (start+1, i−1), right = (i, end). Base case start > end → None. Always return root. O(n²) because of the scan.

Part B · Optimal: one pass with an upper bound

1The question

Same problem. Now we want to stop searching for the split point. The extra scan is the only thing making Part A O(n²).

2Constraints

Same as Part A. The goal now is O(n), which is what an interviewer will expect.

3Intuition: take values one by one and ask "do you fit here?"

Don't split the list at all. Walk through it with one pointer i, from left to right, and place each value as you go:

Doubt: how can I be sure 5 is 8's left child? Couldn't it be 7's left child, with 7 under 8?
→ If the tree were 8 → 7 → 5 (5 under 7), preorder would list 7 before 5: 8 7 5 …. Our list says 8 5 1 7, so 5 comes right after 8. In preorder, the value right after a node, if it's smaller, must be that node's left child. The order of the list already pins down the shape. We just follow it.

4Building the conditions from examples

What bound does a left child get?

Going to 8's left, everything placed there must be smaller than 8. So the left call gets bound = root.val (8).

What bound does a right child get?

Going to a node's right, values can be bigger than the node. But how much bigger? The teacher's reasoning: the right child inherits its parent's bound. Say a node's own bound is 20 (it lives in the left subtree of 20). Its right child can be bigger than the node, but it's still inside 20's left subtree, so it must stay under 20. For the root, the bound is ∞, so the root's right child is limited only by ∞.

Bound rules start: build(bound = ∞) · left child: bound = root.val · right child: bound = the same bound this node got
Doubt: we only check an upper bound. Who makes sure a right child is bigger than its parent?
→ The order of the calls does it for free. We call the left side first, and the left side keeps taking values as long as they're under root.val. It only gives up when the next value is bigger than root.val (or the list has ended). So whatever value reaches the right call has already been rejected for being bigger than the node. A lower-bound check would never fail, so we don't need one.

The two "don't place it" conditions

  1. The list is used up: i == len(preorder). There's no value left to place, so return None. If the values are smaller and smaller (all going left), the bound check would never fail, and without this check we'd read past the end of the list.
  2. The value doesn't fit: preorder[i] > bound. It can't live in this spot → return None, and don't move i. The value stays waiting for the right spot.
Doubt: why check i == len(preorder) before preorder[i] > bound?
→ If i is already past the end, preorder[i] crashes with an index error. Python's or stops as soon as the first part is True, so putting the index check first means we never read a value that doesn't exist. The teacher stresses this order.
Doubt: why move i forward only after creating a node?
→ i points at "the next value still waiting for a home". It moves only when that value actually gets a home. When 7 is rejected under 1 (bound 1) and again on 1's right (bound 5), i stays on 7, so the next spot (5's right, bound 8) gets to try 7 and accepts it.

And as in Part A, every call ends with return root, so the node gets attached to its parent on the way back.

5Approach steps

  1. Keep a shared pointer self.i = 0.
  2. build(bound): if i == n or preorder[i] > bound → return None.
  3. Make root = TreeNode(preorder[i]) and move i forward.
  4. root.left = build(root.val)
  5. root.right = build(bound), using the same bound this call received.
  6. Return root. Start with build(∞).

6Code (Python)

BST from Preorder, optimal (upper bound)
class Solution:
    def bstFromPreorder(self, preorder):
        self.i = 0                               # next value waiting for a spot
        return self.build(preorder, float('inf'))

    def build(self, preorder, bound):
        if self.i == len(preorder) or preorder[self.i] > bound:
            return None                          # nothing left / doesn't fit here
        root = TreeNode(preorder[self.i])
        self.i += 1
        root.left = self.build(preorder, root.val)   # left: must stay under me
        root.right = self.build(preorder, bound)     # right: my parent's limit
        return root

7Code line by line

linewhat it means
self.i = 0One pointer shared by every call. It moves through the list exactly once.
self.build(preorder, float('inf'))The root has no limit.
if self.i == len(preorder)All values are placed, so no more nodes. This check comes first so the next part can't crash.
or preorder[self.i] > bound: return NoneThe waiting value is too big for this spot. Leave the spot empty and let the value try a spot higher up.
root = TreeNode(preorder[self.i]) self.i += 1It fits, so make the node and mark the value as used.
root.left = self.build(preorder, root.val)Fill the left spot. Anything there must be smaller than me.
root.right = self.build(preorder, bound)Fill the right spot. It can be bigger than me, but must respect the limit I was given.
return rootAttaches this subtree to the parent.

8Dry run: the teacher's full walk with bounds

preorder = [8, 5, 1, 7, 10, 12]. "b" = the bound for that call. i = index of the waiting value.

  1. build(b=∞), i=0 → 8 ≤ ∞ → create 8, i=1 → go left with b=8.
  2. build(b=8) → 5 ≤ 8 → create 5 (not yet attached), i=2 → go left with b=5.
  3. build(b=5) → 1 ≤ 5 → create 1, i=3 → go left with b=1.
  4. build(b=1) → value 7 > 1 → None. 1.left = None. i stays at 3 (7 still waiting).
  5. 1 goes right with its parent's bound, b=5 → 7 > 5 → None. 7 can't be 1's right child, because 1 sits in 5's left subtree. 1 returns itself → 5.left = 1.
  6. 5 goes right with its bound, b=8 → 7 ≤ 8 → create 7, i=4.
  7. 7 left (b=7): 10 > 7 → None. 7 right (b=8): 10 > 8 → None. 7 returns → 5.right = 7. 5 returns → 8.left = 5.
  8. 8 goes right with b=∞ → 10 ≤ ∞ → create 10, i=5.
  9. 10 left (b=10): 12 > 10 → None. 10 right (b=∞): 12 → create 12, i=6.
  10. 12 left (b=12): i == 6 == n → None. 12 right (b=∞): i == n → None. 12 returns → 10.right = 12. 10 returns → 8.right = 10.
  11. 8 returns to the main function. The stack is empty. The tree is Example 1's tree ✓
step 4 (deepest)
build(∞) → 8build(8) → 5build(5) → 1build(1): 7 > 1 → None
step 5
build(∞) → 8build(8) → 5build(5) → 1build(5): 7 > 5 → None
step 6
build(∞) → 8build(8) → 5build(8) → 7 ✓

Notice how 7 tried 1's left (no), then 1's right (no), then found its place at 5's right. Failed tries cost O(1) each.

9Complexity & remember

Remember the optimal wayOne pointer, one pass. build(bound): stop if the list is done or the value > bound. Else make the node, i += 1, left gets root.val, right gets the same bound. Start with ∞.

Part C · Revision page

A · split pointB · upper bound
functionbuild(start, end)build(bound) + shared i
base casestart > end → Nonei == n or preorder[i] > bound → None
how the left part is foundscan while smaller than the rootleft call takes values while they're ≤ root.val
left call(start+1, i−1)bound = root.val
right call(i, end)bound = parent's bound
timeO(n²)O(n)
spaceO(h)O(h)
If you remember only 5 lines 1. Preorder = root first, then the left block (all smaller), then the right block (all bigger).
2. Brute: scan to find where the smaller block ends, recurse on both index ranges. O(n²).
3. Optimal: one pointer. Each spot has an upper bound. Too big → None, and the pointer doesn't move.
4. Left child bound = node's value · right child bound = node's own bound · root = ∞.
5. Check "list used up" before reading the value, and always return root.
Mistakes to avoid ✗ while i < end with an inclusive end (misses the last value, breaks [3, 2, 1])
✗ base case "root is None" instead of index-based in the brute force
✗ giving the right child bound = root.val (that's the left child's rule)
✗ moving i when a value is rejected
✗ reading preorder[i] before checking i == n
✗ forgetting return root (nodes get created but never attached)
test it yourself (paste under either solution above)
def pre(r):
    return [] if r is None else [r.val] + pre(r.left) + pre(r.right)
def ino(r):
    return [] if r is None else ino(r.left) + [r.val] + ino(r.right)

for p in ([8, 5, 1, 7, 10, 12], [1, 3], [3, 2, 1], [5]):
    t = Solution().bstFromPreorder(p)
    print(pre(t) == p, ino(t) == sorted(p))   # True True each time

Based on this video: Construct Binary Search Tree from Preorder Traversal