Trees & Graphs
Intermediate 11 min read

Recursion on trees (height, diameter, LCA)

In one line

Most tree problems are solved by asking each subtree a question, trusting the recursive answer, and combining the left and right answers at the node.

What it is

A tree is recursive by nature: each child is the root of a smaller tree. So a function on a tree usually handles the empty tree as a base case, calls itself on the left and right children, and combines the two results. If you trust that the calls return correct answers for the subtrees, you only have to reason about one node.

Three classic examples show the idea. Height (maximum depth): 1 plus the larger child height. Diameter: the longest path between any two nodes, which at its highest point bends through some node, so it equals left height plus right height at that node. Lowest common ancestor (LCA): the deepest node that has both target nodes in its subtree.

Information can flow down (pass depth, a running sum, or bounds as arguments, called top-down) or up (return heights, sizes or flags, called bottom-up). Many hard problems combine both, or track a global best while returning something different to the parent.

Why interviewers ask

These are some of the most common coding interview questions. They test whether you can define what a recursive call returns, pick base cases, and separate what you return to the parent from what you record as the answer. Diameter and maximum path sum are favourite follow-ups exactly because the return value and the answer differ.

How it works

  1. 1Define the function precisely, for example: height(node) returns the number of nodes on the longest path from node down to a leaf, and 0 for an empty node.
  2. 2Write the base case for the empty node (and sometimes for a leaf).
  3. 3Recurse on both children and trust the results.
  4. 4Combine: height is 1 + max(left, right). For diameter, also update a global best with left + right, the longest path that bends at this node.
  5. 5For LCA in a binary tree: if the node is empty or equals p or q, return it. Otherwise recurse both sides. If both sides return something, this node is the LCA; else pass up whichever side found something.
  6. 6Each node is visited once, so these run in O(n) time and O(h) stack space.

Worked example · Height, diameter and LCA on the same tree

  1. Tree: root 1 with children 2 and 3; node 2 has children 4 and 5.
  2. Heights bottom-up: leaves 4, 5 and 3 return 1 (their children return 0). Node 2 returns 1 + max(1, 1) = 2. Node 1 returns 1 + max(2, 1) = 3.
  3. Diameter: at node 2, left + right = 1 + 1 = 2 edges (path 4-2-5). At node 1, left + right = 2 + 1 = 3 edges (path 4-2-1-3). Best is 3.
  4. LCA(4, 5): at node 2, the left call returns 4 and the right call returns 5. Both sides found something, so node 2 is the answer and is passed up to the root, whose right side returns nothing.
  5. LCA(4, 3): at node 2 only the left side finds 4, so 2 passes 4 up. At node 1, the left side returns 4 and the right side returns 3, so node 1 is the LCA.

Picture it

It is like a manager asking each team lead for one number, say the size of their team, adding their own headcount and passing the total up. Nobody needs to see the whole company; each person trusts the numbers from below.

Reference code

def max_depth(node):
    if not node:
        return 0
    return 1 + max(max_depth(node.left), max_depth(node.right))

def diameter(root):
    best = 0
    def height(node):
        nonlocal best
        if not node:
            return 0
        l, r = height(node.left), height(node.right)
        best = max(best, l + r)       # longest path bending here, in edges
        return 1 + max(l, r)          # parent only gets one side
    height(root)
    return best

def lca(node, p, q):
    if node is None or node is p or node is q:
        return node
    left = lca(node.left, p, q)
    right = lca(node.right, p, q)
    if left and right:
        return node                   # p and q are on different sides
    return left or right

max_depth is the plain bottom-up template. diameter returns a height to the parent but records left + right as the answer, which counts edges. lca assumes both p and q are in the tree; each runs in O(n) time and O(h) stack space.

Key patterns

Bottom-up return value

When: Height, size, balanced check, subtree sums

How: Return a value from each child and combine. For a balanced check, return -1 to signal unbalanced and stop early.

Global best, different return

When: Diameter, maximum path sum, longest univalue path

How: Return the best one-sided path to the parent; update a separate global answer with the two-sided path through the current node.

Top-down state

When: Path sum to a target, max value on the path, good nodes count

How: Pass the running state as an argument; check it at leaves or at each node.

Search both sides and report up

When: LCA in a binary tree, find whether a node exists

How: Return the found node (or None); the first node where both sides report is the meeting point.

Real-world use

Git merge base

git merge-base finds the best common ancestor of two commits in the commit graph, the same LCA idea, so a three-way merge knows which version both branches started from.

Directory sizes and depths

Disk usage tools compute each folder's total size as the sum of its children's sizes, a bottom-up tree recursion, and limit output with a max depth passed top-down.

Taxonomies and org charts

Biology and product-category taxonomies use LCA to find the most specific shared category of two items, for example the closest common category of two products for recommendations.

Phylogenetic trees and network latency

Tree diameter style computations give the longest path in a tree, such as the worst-case hop count between two nodes in a tree-shaped network topology.

In a project

You maintain an internal HR tool that shows the org chart. Product wants: who is the closest shared manager of two employees, how deep is each team, and what is the longest reporting chain.

  1. Load the reporting lines into a tree keyed by employee id, with the CEO as root.
  2. Compute team depth with a bottom-up height function and store it on each manager node.
  3. Answer closest shared manager with LCA. For many queries, store each node's depth and parent and walk the deeper one up first, or precompute binary lifting tables for O(log n) per query.
  4. Find the longest chain with the diameter technique, tracking the best left plus right at each manager.
  5. Use an iterative version or raise the recursion limit carefully for very deep hierarchies.

Impact: All three answers come from one or two O(n) passes over the org chart instead of slow per-request database walks, so the page loads instantly even for 50,000 employees, and approval routing can use the shared manager automatically.

Trade-offs

Use it when

  • The answer for a node can be built from answers for its children
  • The tree height is moderate, so recursion depth is safe
  • You want short, readable code in an interview

Avoid it when

  • The tree can be a long chain of tens of thousands of nodes in Python: rewrite with an explicit stack or postorder iteration
  • You need many LCA queries on a static tree: precompute (binary lifting or Euler tour) instead of O(n) per query
  • You are recomputing heights inside another recursion, which turns O(n) into O(n^2); return them in one pass

Cost: Height, diameter and LCA each visit every node once: O(n) time, O(h) space for the call stack.

Common mistakes

  • Calling a separate height function at every node for diameter or balance checks, which is O(n^2) on a skewed tree.
  • Returning the two-sided path to the parent: a parent can only extend one side of the path.
  • Mixing up counting nodes vs edges for height and diameter; say which you use (LeetCode's diameter counts edges).
  • Forgetting that the LCA can be one of the two nodes itself (LCA of a node and its descendant is the node).
  • Using the BST shortcut for LCA on a general binary tree, where values are not ordered.

Interview questions

Quick revision

  • Template: base case for None, recurse left and right, combine.
  • Height = 1 + max(left, right); empty = 0.
  • Diameter = max over nodes of left height + right height (edges).
  • Return one side to the parent, record both sides globally.
  • LCA: return node if None, p or q; both sides non-null means this is the LCA.
  • BST LCA: walk down until p and q split.
  • O(n) time, O(h) stack; watch deep chains in Python.

Check yourself

1. In the diameter algorithm, what does the helper return to its parent?

2. For root 1 with children 2 and 3, where 2 has children 4 and 5, what is LCA(5, 2)?

3. A naive balanced-tree check calls height() at every node. What is its worst-case time?

4. What is the diameter, in edges, of a tree that is a single chain of 5 nodes?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free