<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. DSA
  3. Foundations
All lessons in this course

Lesson 5 of 50 15 minBeginner

Recursion, call trees and memoisation

See recursion as a tree of calls, work out its cost from the tree, fix repeated work with memoisation, and recognise the recursion patterns behind famous algorithms.

Many DSA algorithms (tree traversal, merge sort, quick sort, depth-first search, backtracking, dynamic programming) are recursive. To analyse them you need one new picture: the call tree.

The cost of recursion = number of calls × work per call

The Fibonacci numbers are 0, 1, 1, 2, 3, 5, 8, 13…: each is the sum of the two before. The definition turns directly into code:

Fib.cs
long calls = 0;                      // counts every call long Fib(int n){    calls++;    if (n <= 1) return n;            // base cases: Fib(0) = 0, Fib(1) = 1    return Fib(n - 1) + Fib(n - 2);  // two recursive calls} Console.WriteLine($"Fib(30) = {Fib(30)}, calls = {calls:N0}");
Output
Fib(30) = 832040, calls = 2,692,537

Short, correct, and terribly slow. Each call does O(1) work, but there are millions of calls. Draw the calls as a tree and the reason is obvious:

Interactive

The call tree of Fib(n)

Each circle is one call. Click a circle to highlight every call with the same argument, or press Play to see the calls in the order they happen.

543210121032101
15 calls9 repeated calls
Fib(5) makes 15 calls, and 9 of them repeat work already done. Each extra n multiplies the calls by about 1.6, so Fib(40) needs over 300 million calls: O(2ⁿ).

Every call splits into two, so the tree roughly doubles in size with each extra n: O(2ⁿ) calls (more precisely about 1.6ⁿ). Most of them are repeats: Fib(2) is calculated again and again.

Memoisation: remember answers you already have

If a recursive method is called with the same argument many times and always returns the same answer, store each answer the first time and look it up after that. This is called memoisation (from “memo”, a note to yourself).

FibMemo.cs
static long Fib(int n, Dictionary<int, long> memo){    if (n <= 1) return n;    if (memo.TryGetValue(n, out long known)) return known;   // already solved     long result = Fib(n - 1, memo) + Fib(n - 2, memo);    memo[n] = result;                                         // remember it    return result;} Console.WriteLine(Fib(90, new Dictionary<int, long>()));
Output
2880067194370816120

Now each Fib(k) is calculated once: about 2n calls, O(n) time and O(n) memory for the memo and the stack. Fib(90) is instant, while the plain version would need about 9 billion billion calls.

Or skip recursion: build up from the bottom

Once you see that each value depends only on the two before it, you can compute from the bottom up with a loop and keep just two variables: O(n) time and O(1) space. This bottom-up style is the heart of dynamic programming, which has its own lessons later.

FibLoop.cs
static long Fib(int n){    long a = 0, b = 1;               // Fib(0), Fib(1)    for (int i = 0; i < n; i++)        (a, b) = (b, a + b);         // slide the window forward    return a;} Console.WriteLine(Fib(90));
Output
2880067194370816120
VersionTimeExtra space
Plain recursionO(2ⁿ)O(n) stack
Recursion + memoO(n)O(n) memo + stack
Bottom-up loopO(n)O(1)

Recursion patterns you will meet again

The shape of the call tree tells you the cost. These four shapes cover most algorithms in this course:

PatternEach call…CostExample
Linearmakes one call on n − 1O(n)Sum of a list, factorial
Halvingmakes one call on n / 2O(log n)Binary search, fast power
Divide and conquermakes two calls on n / 2, plus O(n) workO(n log n)Merge sort
Branchingmakes two calls on n − 1 (or n − 1 and n − 2)O(2ⁿ)Plain Fibonacci, all subsets

Here is a halving recursion. To compute xⁿ, compute x^(n/2) once and square it. Each call halves n, so it needs only about log₂ n calls instead of n multiplications:

Power.cs
static long Power(long x, int n){    if (n == 0) return 1;    long half = Power(x, n / 2);     // ONE recursive call on n/2    return n % 2 == 0 ? half * half : half * half * x;} Console.WriteLine(Power(2, 10));Console.WriteLine(Power(3, 5));
Output
1024
243
Quick check

A recursive method makes one call on n / 2 and does O(1) other work. What is its time complexity?

Quick check

Why is plain recursive Fibonacci slow?

Practice

You can climb a staircase 1 or 2 steps at a time. Write Ways(int n) that returns how many different ways there are to reach step n (Ways(0) = 1, Ways(1) = 1). Make it fast enough for n = 50 using memoisation.

Show a hint

The last move was either 1 step (from n − 1) or 2 steps (from n − 2), so Ways(n) = Ways(n − 1) + Ways(n − 2). Store answers in a long[] of size n + 1.

Show the solution
Solution.cs
static long Ways(int n, long[] memo){    if (n <= 1) return 1;    if (memo[n] != 0) return memo[n];    return memo[n] = Ways(n - 1, memo) + Ways(n - 2, memo);} Console.WriteLine(Ways(10, new long[11]));Console.WriteLine(Ways(50, new long[51]));
Output
89
20365011074

Remember

  • Draw recursion as a call tree: cost = number of calls × work per call.
  • Two calls on n − 1 give an exponential tree; one call on n / 2 gives O(log n).
  • Memoisation stores each answer the first time and turns repeated work into lookups.
  • Bottom-up loops can give the same speed with O(1) space; this leads to dynamic programming.
0 of 50 lessons completed