Skip to content

Intermediate: core algorithms · Trees and binary search trees · topic 33 of 61 · 40 min

Recursive tree patterns

Explanation

Most tree problems are solved by one recursive shape: ask the left and right subtrees for an answer, combine them at the node, and return the result upward. With it you can find the height and the diameter, check whether a tree is balanced in O(n), test a root-to-leaf path sum, mirror a tree, and compare two trees.

Trust the recursion

A tree is made of smaller trees: every node is the root of its own subtree. So a tree question can usually be answered by asking the same question about the left subtree and the right subtree, then combining the two answers at the current node. You do not trace every call in your head. You only check three things: the base case (what does an empty tree return?), the combine step (given correct answers from both children, is my answer correct?), and that each call is on a smaller tree.

There are two directions of information. Values can flow UP (a child returns its height to its parent; this is postorder thinking) or DOWN (a parent passes the remaining target sum to its children as an argument; this is preorder thinking). Many problems need both.

Height and diameter

The height of a tree (counted in nodes here) is 1 + the larger of the two child heights, with 0 for an empty tree. The diameter is the number of EDGES on the longest path between any two nodes. That path does not have to pass through the root. At each node, the longest path that bends at this node goes down the left side and down the right side, so its length is left_height + right_height. The diameter is the largest such value over all nodes.

The trick is to compute both at once. The helper returns the height to its parent and, as a side effect, updates a best-so-far variable. That is one visit per node: O(n) time and O(h) stack space.

python
def diameter(root):
    best = 0

    def height(node):              # height in nodes
        nonlocal best
        if node is None:
            return 0
        left = height(node.left)
        right = height(node.right)
        best = max(best, left + right)   # edges on the path bending here
        return 1 + max(left, right)

    height(root)
    return best

print(diameter(build([1, 2, 3, 4, 5])))   # Output: 3  (4 -> 2 -> 1 -> 3)
nonlocal lets the inner function change best from the outer function.

Is the tree balanced, in O(n)

A tree is height-balanced when, at EVERY node, the left and right heights differ by at most 1. The slow way calls a separate height() function at every node; each call walks the whole subtree again, which is O(n²) on a skewed tree. The fast way returns the height upward and uses a special value, −1, to mean "already unbalanced below". As soon as a child reports −1, pass −1 straight up.

python
def check(node):
    if node is None:
        return 0
    left = check(node.left)
    if left == -1:
        return -1
    right = check(node.right)
    if right == -1 or abs(left - right) > 1:
        return -1
    return 1 + max(left, right)

def is_balanced(root):
    return check(root) != -1

print(is_balanced(build([3, 9, 20, None, None, 15, 7])))        # Output: True
print(is_balanced(build([1, 2, 2, 3, 3, None, None, 4, 4])))    # Output: False

Passing values down: path sum

Path sum asks: is there a path from the root down to a LEAF whose values add up to a target? Pass the remaining amount down. At each node subtract its value; at a leaf, check whether the remaining amount equals the leaf's value. Values can be negative, so you cannot stop early just because the running total passed the target.

python
def has_path_sum(node, target):
    if node is None:
        return False
    if node.left is None and node.right is None:      # a leaf
        return node.val == target
    rest = target - node.val
    return has_path_sum(node.left, rest) or has_path_sum(node.right, rest)

t = build([5, 4, 8, 11, None, 13, 4, 7, 2, None, None, None, 1])
print(has_path_sum(t, 22), has_path_sum(t, 26), has_path_sum(t, 9))   # Output: True True False
22 is 5+4+11+2 and 26 is 5+8+13. 9 is 5+4, but 4 is not a leaf.

Changing and comparing trees

Inverting (mirroring) a tree swaps the left and right child of every node. The recursive version is three lines: invert both subtrees, then swap them. Two trees are the same when both are empty, or both roots have equal values and their left subtrees are the same and their right subtrees are the same. A tree is symmetric when its left subtree is a mirror of its right subtree: compare a.left with b.right and a.right with b.left.

python
def invert(node):
    if node is None:
        return None
    node.left, node.right = invert(node.right), invert(node.left)
    return node

def is_mirror(a, b):
    if a is None and b is None:
        return True
    if a is None or b is None:
        return False
    return a.val == b.val and is_mirror(a.left, b.right) and is_mirror(a.right, b.left)

s = build([1, 2, 2, 3, 4, 4, 3])
print(is_mirror(s.left, s.right))   # Output: True  (this tree is symmetric)

Cost

Each of these functions visits every node at most once with O(1) work there: O(n) time. The extra space is the recursion stack, O(h): O(log n) for a balanced tree, O(n) for a skewed one. For trees deeper than about 1,000 levels Python raises RecursionError, so mention an iterative version (with a stack) in the interview.

Common mistakes

  • Thinking the diameter always passes through the root. Fix: update a best value at EVERY node.
  • Mixing edges and nodes in the diameter. With heights in nodes, left + right is already the number of edges.
  • Checking balance only at the root. Fix: every node must satisfy the rule; the −1 signal handles this.
  • Treating any node as the end of a path in path sum. The path must end at a leaf (no children). An empty tree has no path, even for target 0.
  • Swapping children with two separate assignments: node.left = invert(node.right) followed by node.right = invert(node.left) uses the already changed left. Fix: swap in one tuple assignment.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Recursive tree patterns · 1/8

Recursive tree patterns

This lesson shows the one recursive shape behind most tree problems.

Prefer a video by a teacher? Find free videos on “binary tree diameter balanced tree path sum invert tree recursion interview” on YouTube ↗

In real life: Rolling up a team budget in an org chart

A finance tool needs the total salary cost under every manager, so each team lead sees their team's budget. That is the "return values up" pattern: a person's total is their own salary plus the totals of everyone who reports to them. One postorder walk computes every manager's total in O(n), instead of re-adding each subtree from scratch.

python
reports = {"Asha": ["Ravi", "Meena"], "Ravi": ["Kiran"], "Meena": [], "Kiran": []}
salary = {"Asha": 300, "Ravi": 150, "Meena": 140, "Kiran": 80}
total = {}

def roll_up(person):
    total[person] = salary[person] + sum(roll_up(r) for r in reports[person])
    return total[person]

roll_up("Asha")
print(total)
Shows {'Kiran': 80, 'Ravi': 230, 'Meena': 140, 'Asha': 670}: amounts in thousands of rupees a month.

Practice (0/4 done)

Task 1 · Code · Python

Write diameter(root) that returns the number of EDGES on the longest path between any two nodes of the tree. The path may or may not pass through the root. An empty tree and a single node both return 0. Visit each node once (O(n)). Example: diameter(build([1, 2, 3, 4, 5])) returns 3.

Task 2 · Code · Python

Write is_balanced(root) that returns True if, at EVERY node, the heights of the left and right subtrees differ by at most 1. An empty tree is balanced. It must be O(n): do not call a separate height function from every node. Example: is_balanced(build([3, 9, 20, None, None, 15, 7])) returns True.

Task 3 · Code · Python

Write has_path_sum(root, target) that returns True if there is a path from the root down to a LEAF whose values add up to target. Values and target can be negative. An empty tree returns False (even for target 0). Example: for build([5, 4, 8, 11, None, 13, 4, 7, 2, None, None, None, 1]) and target 22 it returns True (5 + 4 + 11 + 2).

Task 4 · Code · Python

Write invert_tree(root) that mirrors the tree IN PLACE (swap the left and right child of every node) and returns the same root object (None for an empty tree). After inverting, the inorder traversal of the tree is the reverse of the original inorder. Example: build([4, 2, 7, 1, 3, 6, 9]) becomes the tree build([4, 7, 2, 9, 6, 3, 1]).

Quick quiz

Question 1

1.Why does the diameter function update best at every node, not only at the root?

Question 2

2.Calling a separate height() at every node to check balance costs, in the worst case…

Question 3

3.For has_path_sum, the tree is build([1, 2]) and the target is 1. What is the right answer?

Question 4

4.To check if a tree is symmetric, which pairs do you compare?

Question 5

5.What is the extra space of a recursive tree function on a balanced tree of n nodes?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why does the diameter function update best at every node, not only at the root?

Answer: Because the longest path may bend at a node below the root

If the root has one short side, the longest path can lie completely inside one subtree.

Calling a separate height() at every node to check balance costs, in the worst case…

Answer: O(n²)

Each height() call walks its whole subtree. On a skewed tree that is n + (n − 1) + … = O(n²). Returning −1 upward makes it O(n).

For has_path_sum, the tree is build([1, 2]) and the target is 1. What is the right answer?

Answer: False, because the root is not a leaf

The path must end at a leaf. The only root-to-leaf path is 1 → 2, which sums to 3.

To check if a tree is symmetric, which pairs do you compare?

Answer: a.left with b.right and a.right with b.left

A mirror flips sides, so outer children match outer children and inner match inner.

What is the extra space of a recursive tree function on a balanced tree of n nodes?

Answer: O(log n)

The recursion stack holds one path from the root, and a balanced tree has height O(log n).

Why is searching down one path O(log n) in a balanced tree but O(n) in a skewed one?Related

Answer: The cost is O(height), and the height is about log n when balanced but n − 1 when skewed

Walking down one path costs one step per level, and the number of levels is the height.

From Trees: words and shapes

Which traversal frees a tree safely?Related

Answer: Post-order

Children must be freed before the parent.

From Binary search trees

Why is checking only node.left.val < node.val < node.right.val not enough to validate a BST?Related

Answer: A value deep in a subtree can break the rule with an ancestor, not its parent

In build([5, 4, 6, None, None, 3, 7]), 3 is fine next to 6 but is smaller than 5 while in 5's right subtree.

From Binary search trees

A perfect binary tree has n nodes. What is the worst extra space of BFS on it?Related

Answer: O(n), because the last level holds about n/2 nodes

The queue holds a whole level at a time, and the bottom level of a perfect tree has about half of all nodes.

From Traversals: DFS and BFS

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Trees and binary search trees” before its exam: Binary search trees, Lowest common ancestor, serialisation and balanced trees.
  2. Module 7 exam: Trees and binary search trees · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Heaps and greedy algorithms. 61 topics left in the course before the final exam.

← Previous: Traversals: DFS and BFS