Part 8 · Lesson 32 · read
Recursion Trees
Draw every call as a tree, so recursion is easy to follow.
On this page
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).
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 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)
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));| n | 4 |
| depth | 1 |
| calls | 1 |
Reading cost off the tree
| Question | Look at… | fib(4) |
|---|---|---|
| Time | the number of nodes × work per node | 9 nodes × O(1) = 9 steps |
| Space (stack) | the height of the tree | height 4 → at most 4 open calls |
| Branching factor (how many branches each node has) | children per node | 2 |
| Leaves | base cases reached | 5 (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.)
// 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])); // 8You 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.
// 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.)
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| Call | n | half = 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.
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.
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 59This 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.
Return the n-th Fibonacci number, where fib(0) = 0, fib(1) = 1 and fib(n) = fib(n − 1) + fib(n − 2).
Hint 1
Hint 2
Show all 3 solutions with explanations
For 0 and 1, return n. For anything else, add the two smaller Fibonacci numbers.
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)); // 55O(2n) time and O(n) space (the height of the tree). This is fine for n up to about 30.
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.
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)); // 12586269025The tree shrinks to about 2n − 1 calls, so it takes O(n) time and O(n) space.
Each value needs only the two before it, so keep just those two.
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)); // 12586269025O(n) time and O(1) space. This is the best of the three.
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.)
Write myPow(x, n). It returns x raised to the power n. The number n can be negative.
Hint 1
Hint 2
Hint 3
Show all 3 solutions with explanations
Loop n times, and each time multiply the result by x.
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.25O(n) time. If n is around 2 billion, this is much too slow.
Work out the half power once and square it. If n is odd, multiply by x one more time.
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)); // 2147483648O(log n) time and O(log n) stack space, because the chain of calls is that long.
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.
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)); // 243O(log n) time and O(1) space. There is no stack at all.
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.)
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.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
Make every row up to row n. Then read position k.
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)); // 1Row n has 2n−1 symbols, so this takes O(2n) time. It is not possible for n = 30.
Ask the parent. The answer is the parent's value, flipped (0 becomes 1, 1 becomes 0) when k is even.
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)); // 0The recursion tree is one chain of n calls. It takes O(n) time and O(n) space, and it never builds a row.
(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.
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.
Hint 1
Hint 2
Show the solution with explanations
hanoi(n − 1, from, via, to)- Save the move
from → to hanoi(n − 1, via, to, from)
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); // 1023There 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.
A classic example where the recursive tree is exactly the work that is needed. No sub-problem repeats, so memoisation would not help.
Return every binary string (a string of only 0 and 1) of length n.
Hint 1
Hint 2
Show the solution with explanations
One child call adds “0” to the string and the other adds “1”. When the string reaches length n, save it.
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); // 8There 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.
This is the simplest first example of backtracking (trying choices and undoing them). Draw the tree for n = 2 first.
Flatten a nested array of numbers (arrays inside arrays, to any depth) into one array. Keep the order.
Hint 1
Hint 2
Show all 2 solutions with explanations
Go through the array. When an element is itself an array, make a recursive call on it.
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]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.
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.
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([])); // []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.
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.)
Write a function that returns how many calls the simple fib(n) makes, including the first call.
Hint 1
Hint 2
Show all 2 solutions with explanations
Write the same recursion, but return the number of nodes instead of the Fibonacci value.
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)); // 2692537The recursion that makes the tree is the same recursion that counts it. (It is slow too, for the same reason.)
The call counts follow the Fibonacci numbers themselves, so you can work them out with a loop.
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)); // 2692537O(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.
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?”.
| Function | Calls per node | Height | Time | Stack space |
|---|---|---|---|---|
| sum of an array | 1 | n | O(n) | O(n) |
| naive fib(n) | 2 | n | O(2ⁿ) | O(n) |
| memoised fib(n) | 2 (but repeated calls are answered at once) | n | O(n) | O(n) |
| fast power | 1 | log n | O(log n) | O(log n) |
| tower of Hanoi | 2 | n | O(2ⁿ) | O(n) |
| binary strings of length n | 2 | n | O(n · 2ⁿ) | O(n) |
Make it stick
- Draw the recursion tree of fib(5) from memory and count its nodes.
- Say how you read the time and the space from a recursion tree.
- Explain the difference between parameter-based and return-based recursion. Give one example of each.
- 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