March 26, 20268 min read

Binary Tree Problems — 10 Patterns for Coding Interviews

The 10 recursive and iterative patterns that cover 90% of binary tree interview questions. DFS, BFS, path sums, construction, and more.

binary tree patterns recursion interview dfs
Ad 336x280

Binary tree problems aren't about trees. They're about recursion patterns. Once you internalize the 10 patterns below, every new tree question becomes "which template does this match?" instead of starting from scratch.

The reason people struggle with tree questions: they try to think iteratively about a recursive structure. Trees are recursion's natural habitat. Lean into it.

Pattern 1: DFS Traversals (Preorder, Inorder, Postorder)

Every tree problem uses one of these traversal orders. The question is which one fits your problem.

  • Preorder (root → left → right): when you need to process the current node before its children. Copying a tree, serialization.
  • Inorder (left → root → right): BSTs. Inorder traversal of a BST gives sorted order.
  • Postorder (left → right → root): when you need children's results before processing the parent. Height calculation, deletion.
def preorder(root):
    if not root:
        return []
    return [root.val] + preorder(root.left) + preorder(root.right)

def inorder(root):
if not root:
return []
return inorder(root.left) + [root.val] + inorder(root.right)

def postorder(root):
if not root:
return []
return postorder(root.left) + postorder(root.right) + [root.val]

function preorder(root) {
  if (!root) return [];
  return [root.val, ...preorder(root.left), ...preorder(root.right)];
}

function inorder(root) {
if (!root) return [];
return [...inorder(root.left), root.val, ...inorder(root.right)];
}

function postorder(root) {
if (!root) return [];
return [...postorder(root.left), ...postorder(root.right), root.val];
}

These are educational implementations. In practice you'd pass a result list by reference instead of creating new arrays at every call.

Pattern 2: BFS (Level-Order Traversal)

Use a queue. Process all nodes at depth d before moving to depth d+1. Critical for "level by level" problems — zigzag traversal, right-side view, level averages.

from collections import deque

def level_order(root):
if not root:
return []
result = []
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result

function levelOrder(root) {
  if (!root) return [];
  const result = [];
  const queue = [root];
  while (queue.length > 0) {
    const level = [];
    const size = queue.length;
    for (let i = 0; i < size; i++) {
      const node = queue.shift();
      level.push(node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    result.push(level);
  }
  return result;
}

The for _ in range(len(queue)) trick processes exactly one level per outer loop iteration. Without it, you'd mix levels together.

Pattern 3: Height / Depth Calculation

Postorder pattern. You need children's heights before you can compute the parent's height.

def height(root):
    if not root:
        return 0
    return 1 + max(height(root.left), height(root.right))
function height(root) {
  if (!root) return 0;
  return 1 + Math.max(height(root.left), height(root.right));
}

This is the building block for balanced tree checks, diameter calculation, and more. If you're computing height from the top down, you're doing it wrong.

Pattern 4: Diameter of Binary Tree

The diameter is the longest path between any two nodes (measured in edges). It doesn't have to pass through the root.

At each node, the longest path through that node is left_height + right_height. Track the global maximum.

def diameter(root):
    best = [0]

def height(node):
if not node:
return 0
l = height(node.left)
r = height(node.right)
best[0] = max(best[0], l + r)
return 1 + max(l, r)

height(root)
return best[0]

function diameter(root) {
  let best = 0;

function height(node) {
if (!node) return 0;
const l = height(node.left);
const r = height(node.right);
best = Math.max(best, l + r);
return 1 + Math.max(l, r);
}

height(root);
return best;
}

The trick: you compute height (return value) but you use both heights to update a separate metric (diameter). This "compute one thing, track another" pattern repeats constantly.

Pattern 5: Path Sum Problems

Three flavors, escalating difficulty:

Has path sum (root to leaf):
def has_path_sum(root, target):
    if not root:
        return False
    if not root.left and not root.right:
        return root.val == target
    return (has_path_sum(root.left, target - root.val) or
            has_path_sum(root.right, target - root.val))
All paths with given sum — same structure, but collect paths:
def path_sum_all(root, target):
    result = []

def dfs(node, remaining, path):
if not node:
return
path.append(node.val)
if not node.left and not node.right and remaining == node.val:
result.append(list(path))
dfs(node.left, remaining - node.val, path)
dfs(node.right, remaining - node.val, path)
path.pop() # backtrack

dfs(root, target, [])
return result

Path sum from any node to any descendant — prefix sum technique on tree paths. That's a different beast and uses Pattern 9 below.

Pattern 6: Lowest Common Ancestor (LCA)

For a regular binary tree (not BST): if p and q are in different subtrees, the current node is the LCA. If both are in the same subtree, recurse into that subtree.

def lca(root, p, q):
    if not root or root == p or root == q:
        return root
    left = lca(root.left, p, q)
    right = lca(root.right, p, q)
    if left and right:
        return root
    return left or right
function lca(root, p, q) {
  if (!root || root === p || root === q) return root;
  const left = lca(root.left, p, q);
  const right = lca(root.right, p, q);
  if (left && right) return root;
  return left || right;
}

Five lines. Runs in O(n). This is one of those algorithms where the code is shorter than the explanation.

For BSTs, you can do better — if both values are less than root, go left. If both are greater, go right. Otherwise, root is the LCA.

Pattern 7: Tree Construction

Build a tree from traversal arrays. The classic: construct from preorder + inorder.

Preorder's first element is the root. Find it in inorder — everything to its left is the left subtree, everything right is the right subtree. Recurse.

def build_tree(preorder, inorder):
    if not preorder or not inorder:
        return None

root = TreeNode(preorder[0])
mid = inorder.index(preorder[0])

root.left = build_tree(preorder[1:mid + 1], inorder[:mid])
root.right = build_tree(preorder[mid + 1:], inorder[mid + 1:])

return root

The O(n) optimization: build a hash map from value → index for the inorder array, and pass indices instead of slicing.

Pattern 8: Serialize / Deserialize

Convert a tree to a string and back. Preorder with null markers is the simplest approach.

def serialize(root):
    if not root:
        return "null"
    return f"{root.val},{serialize(root.left)},{serialize(root.right)}"

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

def build():
val = next(nodes)
if val == "null":
return None
node = TreeNode(int(val))
node.left = build()
node.right = build()
return node

return build()

function serialize(root) {
  if (!root) return "null";
  return ${root.val},${serialize(root.left)},${serialize(root.right)};
}

function deserialize(data) {
const nodes = data.split(",")[Symbol.iterator]();

function build() {
const { value } = nodes.next();
if (value === "null") return null;
const node = { val: parseInt(value), left: null, right: null };
node.left = build();
node.right = build();
return node;
}

return build();
}

Using an iterator avoids index-tracking headaches. The iterator advances naturally through the preorder sequence.

Pattern 9: Prefix Sum on Tree Paths

"Count paths that sum to target from any node to any descendant." This is path sum III (LeetCode 437). Apply the prefix sum idea from arrays, but on root-to-node paths.

def path_sum_iii(root, target):
    count = [0]
    prefix = {0: 1}

def dfs(node, running):
if not node:
return
running += node.val
count[0] += prefix.get(running - target, 0)
prefix[running] = prefix.get(running, 0) + 1

dfs(node.left, running)
dfs(node.right, running)

prefix[running] -= 1 # backtrack

dfs(root, 0)
return count[0]

The backtracking step is what makes this work on trees instead of arrays — when you leave a subtree, you remove that path's contribution from the prefix map.

Pattern 10: Validate BST

The classic "is this a valid BST?" — not just left < root < right, but all left descendants < root < all right descendants.

Pass valid ranges down:

def is_valid_bst(root, lo=float('-inf'), hi=float('inf')):
    if not root:
        return True
    if root.val <= lo or root.val >= hi:
        return False
    return (is_valid_bst(root.left, lo, root.val) and
            is_valid_bst(root.right, root.val, hi))
function isValidBST(root, lo = -Infinity, hi = Infinity) {
  if (!root) return true;
  if (root.val <= lo || root.val >= hi) return false;
  return isValidBST(root.left, lo, root.val) &&
         isValidBST(root.right, root.val, hi);
}

Alternative: inorder traversal should produce a strictly increasing sequence. If it doesn't, not a valid BST.

Complexity Summary

PatternTimeSpace
DFS traversalsO(n)O(h) stack
BFS level-orderO(n)O(w) queue
Height / depthO(n)O(h)
DiameterO(n)O(h)
Path sum (root-to-leaf)O(n)O(h)
LCAO(n)O(h)
ConstructionO(n) with hash mapO(n)
Serialize/DeserializeO(n)O(n)
Prefix sum pathsO(n)O(n)
Validate BSTO(n)O(h)
Where h = height of tree (O(log n) balanced, O(n) worst case), w = max width.

Common Mistakes

Confusing node values with node references in LCA. You're comparing nodes (pointers/references), not their values. Two nodes can have the same value. Off-by-one in BST validation. Using < instead of <= (or vice versa) depending on whether duplicates are allowed. Clarify with the interviewer. Forgetting to backtrack. Path collection and prefix sum problems require undoing your changes when you leave a subtree. Miss the path.pop() and your paths bleed across branches. Using BFS when DFS is simpler. Most tree problems are naturally recursive (DFS). Only reach for BFS when the problem is explicitly level-based.

These 10 patterns cover the vast majority of tree interview questions. Practice them individually on CodeUp until you can identify which pattern applies within 30 seconds of reading a problem.

Ad 728x90