Tree problem patterns

Three recursive shapes that solve most tree questions you will be asked.

Tree questions look varied and reduce to a few skeletons. Recognising which one you need is most of the work.

1. Bottom-up: children answer first

Compute something for each subtree and combine. Use when the parent's answer depends on the children's.

// Balanced check in one pass: -1 means "already unbalanced below here".
function heightOrFail(node) {
  if (!node) return 0;

  const left = heightOrFail(node.left);
  if (left === -1) return -1;

  const right = heightOrFail(node.right);
  if (right === -1) return -1;

  if (Math.abs(left - right) > 1) return -1;
  return 1 + Math.max(left, right);
}

O(n), against O(n log n) for the naive version that recomputes height at every node. The sentinel return value carrying two meanings is a pattern worth stealing.

2. Top-down: pass context to children

Use when a node's answer depends on where it sits — path sums, valid ranges, depth.

function pathsToLeaves(node, path = [], out = []) {
  if (!node) return out;

  path.push(node.value);

  if (!node.left && !node.right) out.push(path.join('->'));
  else {
    pathsToLeaves(node.left, path, out);
    pathsToLeaves(node.right, path, out);
  }

  path.pop();          // backtrack — the same discipline as chapter 6
  return out;
}

3. Global accumulator with a local return

Some problems have an answer that does not pass cleanly up the recursion. Return one thing, record another.

// Diameter: the longest path may not go through the root.
function diameter(root) {
  let best = 0;

  const depth = (node) => {
    if (!node) return 0;

    const left = depth(node.left);
    const right = depth(node.right);

    best = Math.max(best, left + right);      // record: path through this node
    return 1 + Math.max(left, right);         // return: depth, for the parent
  };

  depth(root);
  return best;
}

The same shape solves "maximum path sum", where you also clamp negative contributions to zero.

Lowest common ancestor

Worth knowing outright. In a plain binary tree, return the node where the two targets first appear in different subtrees:

function lca(node, a, b) {
  if (!node || node =<mark> a || node </mark>= b) return node;

  const left = lca(node.left, a, b);
  const right = lca(node.right, a, b);

  return left && right ? node : (left ?? right);
}

In a BST it is easier: walk down while both targets are on the same side, and the first node that splits them is the answer — O(h) with no recursion needed.