Pattern 21 · Trees

Trees & BST

A binary search tree is an ordinary binary tree plus one promise: every value on the left is smaller, every value on the right is larger. That single promise turns lookup into binary search and makes an in-order walk hand you the values already sorted. This page is about tree structure itself — the invariant, height, balance, diameter, and how to rebuild a tree — not about traversal mechanics.

bst search / insert O(h)height · diameter O(n)space O(h) call stackpractice 16 problems
01 core concept

One promise, kept at every node

A binary tree is just nodes with a left and a right. A binary search tree adds one rule, and the rule is stricter than most people remember:

for EVERY node n: every value in n's ENTIRE left subtree < n.val every value in n's ENTIRE right subtree > n.val not just its two children — the whole subtree, all the way down.

That word entire is where most bugs live, and section 02 shows exactly how. Everything else on this page follows from the rule or from the tree's shape.

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

T0The single most useful BST fact

Walk a BST left subtree, then the node, then right subtree — the in-order traversal — and the values come out sorted, ascending. No comparisons, no sorting step. The ordering was baked into the shape when the tree was built.

in_order.pypython
def in_order(node, output):
    if node is None:
        return
    # Everything smaller than node lives to the left, so print it first.
    in_order(node.left, output)
    output.append(node.val)
    # Everything larger lives to the right, so it comes last.
    in_order(node.right, output)


def to_sorted_list(root):
    output = []
    in_order(root, output)
    return output
5 / \ 3 8 in-order visit order: / \ / \ 2 4 7 9 2 3 4 5 7 8 9 <- sorted, for free
1 / 9walkthrough — in-order traversal of a BST comes out sorted
·
·
·
·
·
·
·
output [ ]
rule left → node → right
Our BST has 5 at the root, 3 on the left and 8 on the right, with leaves 2, 4, 7, 9 underneath. The seven boxes below are the output list — empty so far. One box gets filled each time the traversal actually visits a node. Goal: watch what order they come out in.
Why this is worth memorizing
"Kth smallest in a BST" is stop the in-order walk after k nodes. "Validate a BST" is check the in-order output is strictly increasing. "Two Sum on a BST" is in-order into a list, then two pointers. "Minimum absolute difference in a BST" is the smallest gap between in-order neighbours. Four problems, one fact.
02 the variants

Five things this pattern actually does

Getting around a tree is Depth-First Search (Pattern 03) and Breadth-First Search (Pattern 04) — those pages own the traversal machinery. This page owns what you do with the structure once you can walk it.

ASearch and insert: the invariant as a loop

At every node you compare once and discard an entire subtree. That is binary search with pointers instead of indices — O(h), where h is the height. On a balanced tree h ≈ log n; on a degenerate one-sided tree h = n, which is why balance matters so much.

Insert is the same walk: follow the search path until you fall off the tree, then hang the new node in the empty slot you landed on. A newly inserted value is always a leaf.

BValidate: bounds, not parent-child

The natural-looking check — "is my left child smaller than me, is my right child larger" — is wrong, because the invariant is about entire subtrees. The fix is to hand each recursive call the open interval (low, high) its value must fall inside. Going left tightens high to the current value; going right lifts low to it.

10 node allowed interval / \ ---- ---------------- 5 15 10 (-inf, +inf) ok / \ 5 (-inf, 10) ok 6 20 15 (10, +inf) ok 6 (10, 15) 6 > 10 fails! parent-child check: 5<10 ok, 15>10 ok, 6<15 ok, 20>15 ok -> "valid" (WRONG) bounds check: 6 must beat its GRANDPARENT 10 -> invalid (right)
1 / 8walkthrough — validating a BST with (low, high) bounds
10
5
15
6
20
low −∞
high +∞
A different tree: root 10, left child 5, right child 15, and 15 has children 6 and 20. The boxes list the nodes in the order a depth-first check reaches them. Compare each node only with its own parent and everything passes: 5 < 10, 15 > 10, 6 < 15, 20 > 15. Watch why that check is a lie.

CLowest common ancestor: BST vs. general tree

In a BST you never search. Start at the root: if both targets are smaller than the current node, the answer is somewhere to the left; if both are larger, it is to the right. The moment they straddle the current node — one smaller, one larger, or one of them is the current node — you are standing on the LCA. O(h), no recursion needed.

lca_bst.py — LC 235python
def lowest_common_ancestor_bst(root, p, q):
    current = root
    while current is not None:
        if p.val < current.val and q.val < current.val:
            current = current.left       # both targets are in the smaller half
        elif p.val > current.val and q.val > current.val:
            current = current.right      # both targets are in the larger half
        else:
            # They split here (or one of them IS here) — this is the LCA.
            return current
    return None

In a general binary tree there is no ordering to steer by, so you have to look everywhere. Ask both subtrees "did you find either target?" A node that gets a hit from both sides is the split point, so it is the answer. O(n).

lca_general.py — LC 236python
def lowest_common_ancestor(root, p, q):
    # Base case: an empty subtree finds nothing; finding p or q means
    # "the deepest interesting node in here is me".
    if root is None or root is p or root is q:
        return root

    left_result = lowest_common_ancestor(root.left, p, q)
    right_result = lowest_common_ancestor(root.right, p, q)

    if left_result is not None and right_result is not None:
        # One target came from each side, so the paths split right here.
        return root

    # Otherwise pass up whichever side found something (possibly nothing).
    if left_result is not None:
        return left_result
    return right_result

DHeight, balance, diameter: answers that travel upward

These three are the same shape. You recurse to the bottom, and each call returns a small fact about its own subtree to its parent, which combines the two children's facts into its own. That is a post-order computation: do the children first, then yourself. One pass, O(n).

ProblemWhat each call RETURNSWhat it records on the way up
Height (LC 104)1 + max(left, right)nothing
Balanced (LC 110)height, or -1 meaning "already broken"nothing — the sentinel does the work
Diameter (LC 543)heightbest = max(best, left + right)
Max path sum (LC 124)best single downward armbest = max(best, left + right + val)
The return-vs-record split
In diameter and max-path-sum, what you return is not what you answer. A parent can only use one arm of your subtree in its own path, so you return the better single arm. But the longest path through you uses both arms and can never be extended upward — so you record it in a side variable and keep it out of the return value. Mixing these two up is the classic LC 543 / 124 bug.

EStructural comparison and construction

Comparison (LC 100, 572, 617) walks two trees in lockstep: recurse on (a.left, b.left) and (a.right, b.right) together, and handle the case where one side is None and the other is not.

Construction comes in two flavours. From a sorted array (LC 108) you pick the middle element as the root so both halves are the same size — that is what makes the result balanced. From preorder + inorder (LC 105), preorder's first element names the root and inorder tells you how many nodes belong to its left subtree.

sorted: [1, 2, 3, 4, 5, 6, 7] preorder: [3, 9, 20, 15, 7] mid = 4 -> root inorder: [9, 3, 15, 20, 7] 4 3 is the root (first in preorder) / \ inorder splits around 3: 2 6 left = [9] -> 1 node / \ / \ right = [15,20,7] -> 3 nodes 1 3 5 7 recurse with those slices
03 recognition

When to reach for it

  • The words "binary search tree" appear at all — immediately ask yourself whether sorted order or O(h) descent solves it.
  • You need something order-related from a tree: kth smallest, a range of values, the closest value, the smallest difference.
  • The question is about the tree's shape: height, depth, balance, diameter, "is this the same tree", "is this a subtree of that one".
  • You are asked to build or rebuild a tree from a list, or from two traversals.
  • An answer at each node needs facts from both children — that is the post-order "return info up the stack" shape.
When NOT to use it
If the question is really about getting around the structure — shortest path, level by level, "all nodes at distance k", cycles, connectivity — that is Breadth-First Search (Pattern 04) or Depth-First Search (Pattern 03), and those pages have the templates. Also: never use the ordering shortcuts unless the problem says it is a BST. A plain binary tree with numbers in it is not a BST, and LCA-by-comparison will quietly return nonsense on one.
04 templates

The four templates to memorize

T1BST search and insert

bst_search_insert.py — LC 700, 701python
def search_bst(root, target):
    current = root
    while current is not None:
        if target == current.val:
            return current
        if target < current.val:
            # Everything to the right is larger than current, so the target
            # cannot possibly be over there. Discard that whole subtree.
            current = current.left
        else:
            current = current.right
    return None


def insert_into_bst(root, value):
    if root is None:
        return TreeNode(value)

    current = root
    while True:
        if value < current.val:
            if current.left is None:
                # We fell off the tree — this empty slot is where it belongs.
                current.left = TreeNode(value)
                return root
            current = current.left
        else:
            if current.right is None:
                current.right = TreeNode(value)
                return root
            current = current.right

T2Validate a BST with bounds

validate_bst.py — LC 98python
def is_valid_bst(root):

    def check(node, low, high):
        if node is None:
            return True                 # an empty subtree is trivially valid

        # The node must sit strictly inside the window its ancestors left it.
        if node.val <= low or node.val >= high:
            return False

        # Going left tightens the ceiling; going right lifts the floor.
        left_ok = check(node.left, low, node.val)
        right_ok = check(node.right, node.val, high)
        return left_ok and right_ok

    return check(root, float("-inf"), float("inf"))

The in-order alternative is worth knowing too, because it generalizes to LC 230 and LC 501: walk in order and check each value is strictly greater than the previous one.

validate_bst_inorder.py — LC 98, alternativepython
def is_valid_bst_inorder(root):
    stack = []
    current = root
    previous_value = None

    while current is not None or stack:
        # Dive as far left as possible, remembering the way back.
        while current is not None:
            stack.append(current)
            current = current.left

        current = stack.pop()            # this is the next value in sorted order
        if previous_value is not None and current.val <= previous_value:
            return False
        previous_value = current.val

        current = current.right

    return True

T3Post-order: return info up the call stack

height_balance_diameter.py — LC 104, 110, 543python
def height(node):
    """Number of nodes on the longest root-to-leaf path. Empty tree = 0."""
    if node is None:
        return 0
    left_height = height(node.left)
    right_height = height(node.right)
    return 1 + max(left_height, right_height)


def is_balanced(root):
    # Returns the real height, or -1 as a flag meaning "already unbalanced".
    # The flag is what keeps this O(n) instead of O(n^2).
    def height_or_flag(node):
        if node is None:
            return 0

        left_height = height_or_flag(node.left)
        if left_height == -1:
            return -1                    # bad news travels straight up

        right_height = height_or_flag(node.right)
        if right_height == -1:
            return -1

        if abs(left_height - right_height) > 1:
            return -1
        return 1 + max(left_height, right_height)

    return height_or_flag(root) != -1


def diameter_of_binary_tree(root):
    longest_path = 0                     # measured in EDGES, not nodes

    def height(node):
        nonlocal longest_path
        if node is None:
            return 0

        left_height = height(node.left)
        right_height = height(node.right)

        # A path through this node uses both arms, but a parent could only
        # ever use one — so record this here and return only the better arm.
        longest_path = max(longest_path, left_height + right_height)
        return 1 + max(left_height, right_height)

    height(root)
    return longest_path

T4Build a balanced BST from a sorted array

sorted_array_to_bst.py — LC 108python
def sorted_array_to_bst(numbers):

    def build(low, high):
        if low > high:
            return None

        # The middle element splits the slice into two halves of nearly equal
        # size — that even split is exactly what makes the tree balanced.
        middle = (low + high) // 2
        node = TreeNode(numbers[middle])
        node.left = build(low, middle - 1)
        node.right = build(middle + 1, high)
        return node

    return build(0, len(numbers) - 1)
This is the inverse of viz 0
In-order turns a BST into a sorted array; picking midpoints turns a sorted array back into a BST. Each element is used once, so the build is O(n), and the recursion depth is O(log n).

T5Delete from a BST (the fiddly one)

delete_node.py — LC 450python
def delete_node(root, key):
    if root is None:
        return None

    if key < root.val:
        root.left = delete_node(root.left, key)
    elif key > root.val:
        root.right = delete_node(root.right, key)
    else:
        # Found it. Case 1 and 2: zero or one child — promote the child.
        if root.left is None:
            return root.right
        if root.right is None:
            return root.left

        # Case 3: two children. The in-order successor is the smallest value
        # in the right subtree, and it is the only value that can sit here
        # without breaking the invariant. Copy it up, then delete the original.
        successor = root.right
        while successor.left is not None:
            successor = successor.left
        root.val = successor.val
        root.right = delete_node(root.right, successor.val)

    return root
05 common mistakes

Where tree code goes wrong

Mistake 1 — validating with parent-child comparisons
The most-failed easy-looking problem on this page. Checking node.left.val < node.val < node.right.val at every node passes trees that are not BSTs, because a node must beat all of its ancestors, not just its parent. Pass (low, high) down, or check the in-order sequence is increasing.
Mistake 2 — bounds that are not strict, or not infinite
LeetCode's BSTs do not allow duplicates, so the comparison must be strict: <= instead of < silently accepts a tree with two equal values. And initialising the bounds to -2**31 instead of float("-inf") breaks on a tree whose root really is -2147483648 — a favourite hidden test case.
Mistake 3 — recomputing height inside the balance check
Calling a separate height() from inside is_balanced() re-walks every subtree once per ancestor: O(n^2) on a skewed tree. Return the height and the verdict from one recursion — the -1 sentinel in T3 does both with one integer.
Mistake 4 — returning the diameter instead of the height
In LC 543 and LC 124 the recursion must return the one-arm value so the parent can extend it. The two-arm value is the answer, but it is a dead end — no parent can use a path that already turned a corner. Record it in a nonlocal variable and return the arm. Related: LC 543 counts edges, so left + right is already correct — do not add 1.
Mistake 5 — LCA base cases
In the general-tree version, forgetting root is p or root is q means you never report a target that is an ancestor of the other one, and the function returns None for a perfectly valid pair. Also compare by identity, not value, unless the problem guarantees unique values.
Mistake 6 — building a "balanced" tree by inserting in order
Inserting a sorted array into a BST one element at a time produces a right-leaning chain — a linked list with extra steps, height n, and every later search at O(n). Balance comes from choosing the midpoint as the root, which is why LC 108 recurses on halves instead of looping.
10 practice

Practice ladder

In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.

16 problems
easyLC 700
The invariant as a loop: go left when smaller, right when larger. O(h).
easyLC 108
Middle element becomes the root, recurse on both halves. Template T4.
easyLC 617
Walk both trees in lockstep; a missing node means take the other whole subtree.
easyLC 110
Post-order height with −1 as the "already unbalanced" sentinel. Template T3.
easyLC 543
Return height, record left_height + right_height in a side variable.
easyLC 572
is_same_tree as a helper, tried at every node of the big tree.
easyLC 653
In-order into a sorted list, then classic two pointers.
mediumLC 235
Descend until the two targets straddle the current node — that split point is the LCA.
mediumLC 98
Pass (low, high) down; parent-child comparison alone is wrong. Template T2.
mediumLC 230
In-order with an explicit stack, stop counting at k — no need to finish the walk.
mediumLC 701
Follow the search path to a None slot and hang the new leaf there.
mediumLC 173
In-order traversal frozen mid-flight in an explicit stack of left spines.
mediumLC 450
Three cases; the two-child case swaps in the in-order successor.
mediumLC 105
Preorder gives the root, inorder gives the split; hash the inorder indices.
mediumLC 236
No ordering to exploit — search both sides and return the node that sees both.
hardLC 124
Same shape as diameter: return one arm upward, record both arms globally.