Part 8 · Lesson 32 · read

Recursion Trees

Draw every call as a tree, so recursion is easy to follow.

On this page

  1. Every call is a node
  2. Drawing a recursion tree
  3. Traced: the calls of fib(4)
  4. Reading cost off the tree
  5. Passing answers down vs returning answers up
  6. Many recursive calls
  7. Fast power: a tree that is one straight line
  8. Preview: the cost of repeating work
  9. Practice questions (7)
  10. Make it stick
  11. What's next

Every call is a node

Lesson 14 taught recursion as “a function that calls itself on a smaller problem”. Part 8 builds on this, so you need one more picture: the recursion tree. It is like a family tree. Draw every call as a node (a dot or box). Draw an arrow from a call to each call it makes. These are its children. The first call is the root. Calls that stop at the base case (the simplest case, which needs no more calls) are the leaves.

Once you can draw that tree, three questions become easy. What does this function return? Read the leaves, then combine the answers going up. How long does it take? Count the nodes. How much memory does it use? Look at the height of the tree, because that is how many calls are open at the same time.

Drawing a recursion tree

For Fibonacci, each call makes two children: fib(n − 1) and fib(n − 2).

JavaScript
function fib(n) {  if (n <= 1) return n;          // base case: stop here  return fib(n - 1) + fib(n - 2);   // two smaller problems}console.log(fib(4)); // 3
TEXT
                    fib(4)                  /        \             fib(3)          fib(2)            /      \        /     \        fib(2)    fib(1)  fib(1)  fib(0)        /    \    fib(1)  fib(0)

To draw the tree, follow these steps. Write the first call. Below it, write each call it makes. Repeat this for every node that is not a base case. At the leaves, write the base-case value. Then work back up. The value of each node is made from its children (here, the sum of the two).

Traced: the calls of fib(4)

step 1 / 19
Run this line
call #1: fib(4)

A new call starts. It is 1 level down in the tree (the first call is level 1). Calls so far: 1.

▶function fib(n) {2  if (n <= 1) return n;3  const a = fib(n - 1);4  const b = fib(n - 2);5  return a + b;6}7console.log(fib(4));
Variables
n4
depth1
calls1
Console
(nothing printed yet)
Calls run in depth-first order: go all the way down the left branch, come back up, then do the right branch. · interactive
Depth-first order
Depth-first means you go as deep as you can down one branch before you try the next branch. The order in the trace shows how the call stack works. The call stack is the pile of calls that are waiting to finish, like a stack of plates: the last call that started is the first one to finish. The left child runs to the end and returns before the right child starts. At any moment, only the calls on the path from the root to the current node are open.

Reading cost off the tree

Dry run · What the tree tells you
QuestionLook at…fib(4)
Timethe number of nodes × work per node9 nodes × O(1) = 9 steps
Space (stack)the height of the treeheight 4 → at most 4 open calls
Branching factor (how many branches each node has)children per node2
Leavesbase cases reached5 (three fib(1), two fib(0))

For the call stack, the space is the height, not the number of nodes. Calls that have already returned are gone.

General rule: if each call makes b calls and the tree has height h, there are about bh nodes. Fibonacci makes 2 calls each time and has height n, so the tree has at most about 2n nodes. That means O(2n) time (roughly, the time doubles every time n grows by 1) and O(n) space. (The exact count grows a little slower, about 1.6n, because the right side of the tree is shorter. It is still exponential growth.) A function that makes one call per level over n levels (like the sum below) has n nodes, so it takes O(n) time and O(n) space.

Passing answers down vs returning answers up

Information can travel through the tree in two ways.

  • Return-based: each call returns its answer, and the parent combines the answers. The answer is built on the way up. (Examples: sum, Fibonacci, tree height.)
  • Parameter-based: each call receives what has been decided so far as parameters (the values you pass in). The answer is complete at the bottom. The answer travels down. (Examples: a running total, the current path, a result list shared by all calls.)
JavaScript
// Return-based: the answer is built on the way back UP.function sumReturn(arr, i = 0) {  if (i === arr.length) return 0;            // nothing left to add  return arr[i] + sumReturn(arr, i + 1);     // my value + the sum of the rest}// Parameter-based: the answer is carried DOWN and given back at the bottom.function sumParam(arr, i = 0, total = 0) {  if (i === arr.length) return total;        // the total we collected is the answer  return sumParam(arr, i + 1, total + arr[i]);}console.log(sumReturn([3, 1, 4])); // 8console.log(sumParam([3, 1, 4]));  // 8

You will use parameter-based recursion when you need to list things, such as all subsets or all permutations (the next two lessons). The parameters hold the choices made so far. At a leaf, you save the finished choice.

JavaScript
// Parameter-based with one shared result list: common when you must list everything.function evensUpTo(n) {  const out = [];  (function go(i) {    if (i > n) return;    if (i % 2 === 0) out.push(i);    go(i + 1);  })(0);  return out;}console.log(evensUpTo(7)); // [0, 2, 4, 6]

Many recursive calls

When a function makes two or more recursive calls per node, the tree spreads out like a fan. There are two cases.

  • Independent subproblems (merge sort cuts the array into two halves): the pieces do not overlap, so the total work stays small: O(n log n).
  • Overlapping subproblems (Fibonacci): the same sub-tree appears many times, so you do the same work again and again. Memoisation (remembering answers you already worked out, Part 14) removes this repeated work.

Tower of Hanoi is the classic puzzle with 2 calls per node and no overlap. To move n disks, first move n − 1 disks out of the way. Then move the big disk. Then move the n − 1 disks back on top. The calls that move a disk number 2n − 1, one per move, so the number of moves is exactly 2n − 1 (Practice question 4).

Fast power: a tree that is one straight line

To work out xn (n copies of x multiplied together), the simple way takes about n multiplications. For example, 23 = 2 × 2 × 2 = 8. But xn = (xn/2)². So make one recursive call on half the exponent (the small raised number n) and reuse its result. The tree is then one chain of about log₂ n calls. (log₂ n means how many times you can halve n before you reach 1.)

JavaScript
function myPow(x, n) {  if (n < 0) return 1 / myPow(x, -n);       // a negative power means 1 divided by the positive power  if (n === 0) return 1;                    // base case: x^0 = 1  const half = myPow(x, Math.floor(n / 2)); // ONE call on half the size  return n % 2 === 0 ? half * half : half * half * x;}console.log(myPow(2, 10));  // 1024console.log(myPow(2, -2));  // 0.25console.log(myPow(2, 31));  // 2147483648
Dry run · myPow(2, 10): the chain of calls
Callnhalf = result of…Returns
myPow(2, 10)10 (even)myPow(2, 5)32 × 32 = 1024
myPow(2, 5)5 (odd)myPow(2, 2)4 × 4 × 2 = 32
myPow(2, 2)2 (even)myPow(2, 1)2 × 2 = 4
myPow(2, 1)1 (odd)myPow(2, 0)1 × 1 × 2 = 2
myPow(2, 0)0(base case)1

Five calls instead of ten multiplications. For n = 1,000,000 it is about 20 calls instead of a million.

Call it once, not twice
If you write myPow(x, n/2) * myPow(x, n/2), you make two calls per level. Then the tree has n leaves and you are back to O(n). Save the half result in a variable and use that variable twice.

Preview: the cost of repeating work

Let us count the calls. The simple Fibonacci tree for fib(30) has 2,692,537 nodes. If you remember each answer the first time you work it out, the tree shrinks to 59 nodes.

JavaScript
let calls = 0;function fibNaive(n) {  calls++;  return n <= 1 ? n : fibNaive(n - 1) + fibNaive(n - 2);}console.log(fibNaive(20), calls); // 6765 21891calls = 0;const memo = new Map();function fibMemo(n) {  calls++;  if (n <= 1) return n;  if (memo.has(n)) return memo.get(n);        // already solved: reuse the answer  const value = fibMemo(n - 1) + fibMemo(n - 2);  memo.set(n, value);  return value;}console.log(fibMemo(30), calls); // 832040 59

This is memoisation (also spelled memoization). It means remembering the answer of a call, so that the same call is never worked out twice. You keep a Map (a lookup table of key and value pairs) from the inputs to the answers, and you check it at the top of the function. It helps whenever the same call can appear in more than one place in the tree. You will learn it fully in Part 14. For now, just see that the recursion tree shows you when it will help: look for sub-trees that appear more than once.

Practice questions

For every question, draw the tree for a very small input before you write any code.

Q1Fibonacci numberEasy

Return the n-th Fibonacci number, where fib(0) = 0, fib(1) = 1 and fib(n) = fib(n − 1) + fib(n − 2).

Example 1
Input: n = 2
Output: 1
Explanation: fib(2) = fib(1) + fib(0) = 1 + 0.
Example 2
Input: n = 4
Output: 3
Explanation: 0, 1, 1, 2, 3.
Example 3
Input: n = 10
Output: 55
Explanation: The tenth Fibonacci number.
Stuck? Open one hint at a time
Hint 1
fib(0) = 0 and fib(1) = 1. Every other value is the sum of the two values before it.
Hint 2
The plain recursion works, but how many calls does it make when n = 30?
Show all 3 solutions with explanations
Approach 1Plain recursion
The idea

For 0 and 1, return n. For anything else, add the two smaller Fibonacci numbers.

JavaScript
function fib(n) {  if (n <= 1) return n;  return fib(n - 1) + fib(n - 2);}console.log(fib(2));  // 1console.log(fib(4));  // 3console.log(fib(10)); // 55
How it works

O(2n) time and O(n) space (the height of the tree). This is fine for n up to about 30.

Approach 2Remember answers (memoisation)
The idea

Memoisation means remembering answers you already worked out. Save each result in a Map (a lookup table) the first time. Look in the Map before you make a recursive call.

JavaScript
function fib(n, memo = new Map()) {  if (n <= 1) return n;  if (memo.has(n)) return memo.get(n);  const value = fib(n - 1, memo) + fib(n - 2, memo);  memo.set(n, value);  return value;}console.log(fib(30));  // 832040console.log(fib(50));  // 12586269025
How it works

The tree shrinks to about 2n − 1 calls, so it takes O(n) time and O(n) space.

Approach 3A loop with two variables
The idea

Each value needs only the two before it, so keep just those two.

JavaScript
function fib(n) {  let a = 0, b = 1;               // fib(0), fib(1)  for (let i = 0; i < n; i++) {    [a, b] = [b, a + b];  }  return a;}console.log(fib(10)); // 55console.log(fib(0));  // 0console.log(fib(50)); // 12586269025
How it works

O(n) time and O(1) space. This is the best of the three.

Which approach should you use?

Show the recursion first, because it looks just like the definition. Use the tree to explain why it is slow. Then give the loop. (LeetCode 509.)

Q2Pow(x, n)Medium

Write myPow(x, n). It returns x raised to the power n. The number n can be negative.

Example 1
Input: x = 2, n = 10
Output: 1024
Explanation: Ten 2s multiplied together.
Example 2
Input: x = 2, n = −2
Output: 0.25
Explanation: 2⁻² = 1 / 2² = 1/4.
Example 3
Input: x = 2, n = 0
Output: 1
Explanation: Anything to the power 0 is 1.
Stuck? Open one hint at a time
Hint 1
xn = x × xn−1 works, but it takes n steps.
Hint 2
xn = (xn/2)², with one extra x when n is odd. Call the function only once.
Hint 3
A negative power is 1 divided by the same positive power.
Show all 3 solutions with explanations
Approach 1Multiply n times
The idea

Loop n times, and each time multiply the result by x.

JavaScript
function myPow(x, n) {  if (n < 0) { x = 1 / x; n = -n; }  let result = 1;  for (let i = 0; i < n; i++) result *= x;  return result;}console.log(myPow(2, 10)); // 1024console.log(myPow(2, -2)); // 0.25
How it works

O(n) time. If n is around 2 billion, this is much too slow.

Approach 2Fast power by halving (recursion)
The idea

Work out the half power once and square it. If n is odd, multiply by x one more time.

JavaScript
function myPow(x, n) {  if (n < 0) return 1 / myPow(x, -n);  if (n === 0) return 1;  const half = myPow(x, Math.floor(n / 2));  return n % 2 === 0 ? half * half : half * half * x;}console.log(myPow(2, 10)); // 1024console.log(myPow(2, -2)); // 0.25console.log(myPow(2, 0));  // 1console.log(myPow(2, 31)); // 2147483648
How it works

O(log n) time and O(log n) stack space, because the chain of calls is that long.

Approach 3Fast power with a loop
The idea

Write the exponent in binary (base 2, using only 0 and 1). At each step, square x. When the current binary digit is 1, multiply x into the result.

JavaScript
function myPow(x, n) {  if (n < 0) { x = 1 / x; n = -n; }  let result = 1;  while (n > 0) {    if (n % 2 === 1) result *= x;   // this binary digit of the exponent is 1    x *= x;                         // x, x^2, x^4, x^8, ...    n = Math.floor(n / 2);  }  return result;}console.log(myPow(2, 10)); // 1024console.log(myPow(2, -2)); // 0.25console.log(myPow(3, 5));  // 243
How it works

O(log n) time and O(1) space. There is no stack at all.

Which approach should you use?

Interviewers usually want the recursive halving first. Then they often ask, “can you do it without a stack?”, and the loop is the answer. (LeetCode 50.)

Q3K-th symbol in grammarMedium

Row 1 is 0. To make the next row, replace every 0 with 01 and every 1 with 10. Return the k-th symbol of row n. Counting starts at 1.

Example 1
Input: n = 1, k = 1
Output: 0
Explanation: Row 1 is "0".
Example 2
Input: n = 2, k = 1
Output: 0
Explanation: Row 2 is "01". Its first symbol is 0.
Example 3
Input: n = 2, k = 2
Output: 1
Explanation: The second symbol of “01”.
Stuck? Open one hint at a time
Hint 1
The rows are: 0 → 01 → 0110 → 01101001. Each symbol becomes two symbols in the next row: 0 → 01 and 1 → 10.
Hint 2
The k-th symbol of row n came from the ⌈k / 2⌉-th symbol of row n − 1. (⌈ ⌉ means round up.)
Hint 3
If k is odd, it is the first of the pair, so it is the same as the parent. If k is even, it is the second, so it is the opposite of the parent.
Show all 2 solutions with explanations
Approach 1Build the whole row
The idea

Make every row up to row n. Then read position k.

JavaScript
function kthGrammar(n, k) {  let row = "0";  for (let i = 1; i < n; i++) {    row = [...row].map((c) => (c === "0" ? "01" : "10")).join("");  }  return Number(row[k - 1]);}console.log(kthGrammar(1, 1)); // 0console.log(kthGrammar(2, 2)); // 1console.log(kthGrammar(4, 5)); // 1
How it works

Row n has 2n−1 symbols, so this takes O(2n) time. It is not possible for n = 30.

Approach 2Follow the parent (recursion)
The idea

Ask the parent. The answer is the parent's value, flipped (0 becomes 1, 1 becomes 0) when k is even.

JavaScript
function kthGrammar(n, k) {  if (n === 1) return 0;  const parent = kthGrammar(n - 1, Math.ceil(k / 2));  return k % 2 === 1 ? parent : 1 - parent;}console.log(kthGrammar(1, 1));            // 0console.log(kthGrammar(2, 1));            // 0console.log(kthGrammar(2, 2));            // 1console.log(kthGrammar(4, 5));            // 1console.log(kthGrammar(4, 6));            // 0console.log(kthGrammar(30, 434991989));   // 0
How it works

The recursion tree is one chain of n calls. It takes O(n) time and O(n) space, and it never builds a row.

Which approach should you use?

(LeetCode 779.) This is hard to see at first. It becomes clear when you draw the tree and notice that each node has one parent.

Q4Tower of HanoiMedium

Move n disks from peg A to peg C, using peg B as a helper. Move one disk at a time. Never put a larger disk on a smaller disk. Return the list of moves.

Example 1
Input: n = 1
Output: A→C
Explanation: One disk moves straight across.
Example 2
Input: n = 2
Output: A→B, A→C, B→C
Explanation: Put the small disk on B, move the big disk, then bring the small disk back.
Example 3
Input: n = 3
Output: 7 moves
Explanation: 2³ − 1 = 7.
Stuck? Open one hint at a time
Hint 1
To move n disks from A to C: first move n − 1 disks from A to B (using C as helper). Then move the biggest disk from A to C. Then move the n − 1 disks from B to C (using A as helper).
Hint 2
The base case is zero disks. There is nothing to do.
Show the solution with explanations
Approach 1Recursion on the number of disks
The idea
  1. hanoi(n − 1, from, via, to)
  2. Save the move from → to
  3. hanoi(n − 1, via, to, from)
JavaScript
function hanoi(n, from, to, via, moves = []) {  if (n === 0) return moves;  hanoi(n - 1, from, via, to, moves);     // clear the way  moves.push(from + "→" + to);            // move the biggest disk  hanoi(n - 1, via, to, from, moves);     // put the other disks back on top  return moves;}console.log(hanoi(1, "A", "C", "B").join(", ")); // A→Cconsole.log(hanoi(2, "A", "C", "B").join(", ")); // A→B, A→C, B→Cconsole.log(hanoi(3, "A", "C", "B").length);     // 7console.log(hanoi(10, "A", "C", "B").length);    // 1023
How it works

There are 2n − 1 calls that make a move (plus empty calls with 0 disks), so the time is O(2n). You cannot do better, because that many moves are needed. The stack depth is n.

Which approach should you use?

A classic example where the recursive tree is exactly the work that is needed. No sub-problem repeats, so memoisation would not help.

Q5All binary strings of length nEasy

Return every binary string (a string of only 0 and 1) of length n.

Example 1
Input: n = 2
Output: ["00", "01", "10", "11"]
Explanation: Each position is 0 or 1.
Example 2
Input: n = 3
Output: 8 strings
Explanation: 2³ = 8.
Stuck? Open one hint at a time
Hint 1
At each position you make a choice: 0 or 1. So each node has two children.
Hint 2
Pass the string built so far as a parameter. When its length is n, save it.
Show the solution with explanations
Approach 1A tree of choices (parameter-based)
The idea

One child call adds “0” to the string and the other adds “1”. When the string reaches length n, save it.

JavaScript
function binaryStrings(n) {  const out = [];  function build(prefix) {    if (prefix.length === n) { out.push(prefix); return; }    build(prefix + "0");    build(prefix + "1");  }  build("");  return out;}console.log(binaryStrings(2));        // ["00","01","10","11"]console.log(binaryStrings(3).length); // 8
How it works

There are 2n leaves, and each is a string of length n, so the time is O(n · 2n). This tree, where you pick a value at each position, is the template for subsets in the next lesson.

Which approach should you use?

This is the simplest first example of backtracking (trying choices and undoing them). Draw the tree for n = 2 first.

Q6Flatten a nested arrayMedium

Flatten a nested array of numbers (arrays inside arrays, to any depth) into one array. Keep the order.

Example 1
Input: [1, [2, [3, [4]], 5]]
Output: [1, 2, 3, 4, 5]
Explanation: Every nested level is removed.
Example 2
Input: [[], [1], [[2]]]
Output: [1, 2]
Explanation: Empty arrays add nothing.
Stuck? Open one hint at a time
Hint 1
The data is shaped like a tree. An element is either a number (a leaf) or an array (more branches).
Hint 2
For each element: if it is an array, flatten it with a recursive call. Otherwise, keep it.
Show all 2 solutions with explanations
Approach 1Recursion
The idea

Go through the array. When an element is itself an array, make a recursive call on it.

JavaScript
function flatten(arr) {  const out = [];  for (const x of arr) {    if (Array.isArray(x)) out.push(...flatten(x));    else out.push(x);  }  return out;}console.log(flatten([1, [2, [3, [4]], 5]])); // [1, 2, 3, 4, 5]console.log(flatten([[], [1], [[2]]]));      // [1, 2]
How it works

About O(total elements) time when the nesting is shallow. Copying the inner results into the outer one at every level adds some extra work for deep nesting. The recursion depth equals the deepest nesting.

Approach 2An explicit stack (no recursion)
The idea

Put the array's elements on a stack (a list where you add and remove only at the end, so the last item in is the first item out). Take one off. If it is an array, put its elements back on the stack.

JavaScript
function flatten(arr) {  const out = [];  const stack = [...arr];  while (stack.length > 0) {    const x = stack.pop();    if (Array.isArray(x)) stack.push(...x);    else out.push(x);  }  return out.reverse();              // popping goes from right to left, so reverse at the end}console.log(flatten([1, [2, [3, [4]], 5]])); // [1, 2, 3, 4, 5]console.log(flatten([]));                    // []
How it works

It is the same work, but a very deep nesting can no longer cause a stack overflow (the call stack running out of room). Changing recursion into a stack like this comes back in the trees lessons.

Which approach should you use?

Use recursion. Mention the stack version as your answer to “what if the nesting is extremely deep?”. (arr.flat(Infinity) exists, but the question tests the idea.)

Q7How many calls does fib(n) make?Medium

Write a function that returns how many calls the simple fib(n) makes, including the first call.

Example 1
Input: n = 4
Output: 9
Explanation: The tree has 9 nodes (see the trace in the lesson).
Example 2
Input: n = 20
Output: 21891
Explanation: About 22 thousand calls for a small input.
Example 3
Input: n = 30
Output: 2692537
Explanation: More than two and a half million.
Stuck? Open one hint at a time
Hint 1
Let calls(n) be the number of calls. The root is one call, plus all the calls of its two children.
Hint 2
calls(n) = 1 + calls(n − 1) + calls(n − 2), with calls(0) = calls(1) = 1.
Show all 2 solutions with explanations
Approach 1Count by counting the tree
The idea

Write the same recursion, but return the number of nodes instead of the Fibonacci value.

JavaScript
function fibCalls(n) {  if (n <= 1) return 1;                         // a leaf: one call  return 1 + fibCalls(n - 1) + fibCalls(n - 2); // me + both sub-trees}console.log(fibCalls(4));  // 9console.log(fibCalls(20)); // 21891console.log(fibCalls(30)); // 2692537
How it works

The recursion that makes the tree is the same recursion that counts it. (It is slow too, for the same reason.)

Approach 2A formula: 2 × fib(n + 1) − 1
The idea

The call counts follow the Fibonacci numbers themselves, so you can work them out with a loop.

JavaScript
function fibCallsFast(n) {  let a = 0, b = 1;                  // fib(0), fib(1)  for (let i = 0; i <= n; i++) [a, b] = [b, a + b];   // after the loop, a = fib(n + 1)  return 2 * a - 1;}console.log(fibCallsFast(4));  // 9console.log(fibCallsFast(20)); // 21891console.log(fibCallsFast(30)); // 2692537
How it works

O(n) time. The number of calls grows like 1.618n (the golden ratio, about 1.618, to the power n). That is exponential growth, but a little slower than 2n.

Which approach should you use?

This is a thinking exercise, not a LeetCode problem. It teaches you to turn “how long will this take?” into “how many nodes does the tree have?”.

Dry run · Cost of the functions in this lesson
FunctionCalls per nodeHeightTimeStack space
sum of an array1nO(n)O(n)
naive fib(n)2nO(2ⁿ)O(n)
memoised fib(n)2 (but repeated calls are answered at once)nO(n)O(n)
fast power1log nO(log n)O(log n)
tower of Hanoi2nO(2ⁿ)O(n)
binary strings of length n2nO(n · 2ⁿ)O(n)

Make it stick

Make it stick — close this page, then
  1. Draw the recursion tree of fib(5) from memory and count its nodes.
  2. Say how you read the time and the space from a recursion tree.
  3. Explain the difference between parameter-based and return-based recursion. Give one example of each.
  4. Explain why fast power calls itself only once, and what happens if it calls itself twice.

Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.

What's next

With recursion trees you can see how a function explores all the options. Lesson 33 uses a tree whose branches are choices: pick an item or skip it. The leaves of that tree are all the subsets, permutations and combinations.

Practise on LeetCode

Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series