Recursion

Solve a problem by solving smaller copies of it. Base cases, the call stack, and when recursion is the right tool, traced step by step in JS and Python.

15 minLesson 3 of 5
On this page
  1. The idea: solve a smaller copy
  2. Trace it: the call stack
  3. When recursion repeats itself
  4. Recursion or a loop
  5. Common mistakes
  6. Check yourself
  7. What to practise next

You want to know how many files are in your Photos folder. Inside it are some photos, but also folders: "2023", "2024", "Holidays". Inside "Holidays" are more folders, one per trip. Inside those, maybe more. You do not know how deep it goes.

A plain loop struggles here, because you would need one loop per level and you do not know how many levels there are. But there is a very natural way to describe the job: "The number of files in a folder is the files directly in it, plus the number of files in each of its subfolders." That sentence uses the job to describe itself. That is recursion.

The idea: solve a smaller copy

A recursive function is a function that calls itself on a smaller piece of the problem. Every recursive function has two parts:

  • Base case. A piece so small you can answer it directly, without calling yourself. For the folders: a single file counts as 1.
  • Recursive case. Break the problem into smaller pieces, call yourself on each, and combine the answers. For the folders: add up the counts of everything inside.

The base case is what stops the calls. Each recursive call must move closer to it. A subfolder always has fewer things inside than the folder around it, so the calls keep shrinking until they hit single files.

Here a folder is a list, and each item in it is either a file (a string) or another folder (a list). The Photos example might look like ["a", ["b", "c"], "d"]: two files at the top, plus a subfolder holding two more.

function countFiles(item) {
  if (typeof item === "string") return 1; // base case: one file
  let total = 0;                          // recursive case: a folder
  for (const child of item) {
    total += countFiles(child);           // trust the smaller call
  }
  return total;
}
 
countFiles(["a", ["b", "c"], "d"]);                 // 4
countFiles(["a.jpg", ["b.jpg", ["c.jpg", []]], []]); // 3
def count_files(item):
    if isinstance(item, str):
        return 1  # base case: one file
    total = 0     # recursive case: a folder
    for child in item:
        total += count_files(child)  # trust the smaller call
    return total
 
count_files(["a", ["b", "c"], "d"])                  # 4
count_files(["a.jpg", ["b.jpg", ["c.jpg", []]], []])  # 3

Look at how little the function does. It never tries to handle grandchildren or great-grandchildren. It only says "ask each child for its count and add them up". Any depth works, including empty folders: the loop does not run and the count is 0.

Trace it: the call stack

"Trust the smaller call" can feel like magic, but there is a plain mechanism behind it. When a function calls another function, the computer pauses the first one, remembers exactly where it was and what its variables held, and runs the new call. When that call returns, the paused one carries on. The paused calls wait in a pile called the call stack: newest on top, and only the top one is running.

Step through countFiles(["a", ["b", "c"], "d"]). The top row is the call stack, oldest call on the left.

Counting files in nested foldersUse the arrows or ← → keys
countFiles(item):  if item is a file: return 1  total = 0  for each child in item:    total += countFiles(child)  return total
  1. call stack
    0root
    root
    0a
    1[b, c]
    2d
    root total
    0
    calls so far
    1

    1Call countFiles on the whole folder, called root here. It is a folder, not a file, so set its total to 0.

  2. call stack
    0root
    1a
    root
    0achild
    1[b, c]
    2d
    root total
    0
    calls so far
    2

    2The first child is "a". Root pauses and calls countFiles("a"). A new call goes on top of the stack.

  3. call stack
    0root
    1a
    root
    0achild
    1[b, c]
    2d
    root total
    0
    returned
    1
    calls so far
    2

    3"a" is a file. Base case: return 1 straight away. No more calls needed.

  4. call stack
    0root
    root
    0achild
    1[b, c]
    2d
    root total
    1
    calls so far
    2

    4The call for "a" is finished and leaves the stack. Root wakes up where it paused and adds the 1.

  5. call stack
    0root
    1[b, c]
    [b, c]
    0b
    1c
    root total
    1
    [b, c] total
    0
    calls so far
    3

    5The next child is a folder, [b, c]. Root pauses again and calls countFiles([b, c]). That call starts its own total at 0.

  6. call stack
    0root
    1[b, c]
    2b
    [b, c]
    0bchild
    1c
    root total
    1
    [b, c] total
    0
    calls so far
    4

    6Its first child is "b", so [b, c] pauses and calls countFiles("b"). Three calls are on the stack now, and only the top one runs.

  7. call stack
    0root
    1[b, c]
    [b, c]
    0bchild
    1c
    root total
    1
    [b, c] total
    1
    calls so far
    4

    7"b" is a file and returns 1. Its call leaves the stack, and [b, c] adds the 1. Root's total does not change: each call has its own total.

  8. call stack
    0root
    1[b, c]
    2c
    [b, c]
    0b
    1cchild
    root total
    1
    [b, c] total
    1
    calls so far
    5

    8The next child is "c". [b, c] pauses again and calls countFiles("c").

  9. call stack
    0root
    1[b, c]
    [b, c]
    0b
    1cchild
    root total
    1
    [b, c] total
    2
    calls so far
    5

    9"c" is a file and returns 1. [b, c] adds it: its total is 2.

  10. call stack
    0root
    root
    0a
    1[b, c]child
    2d
    root total
    3
    returned
    2
    calls so far
    5

    10[b, c] has no children left. It returns 2 and leaves the stack. Root wakes up and adds it: 1 + 2 = 3.

  11. call stack
    0root
    1d
    root
    0a
    1[b, c]
    2dchild
    root total
    3
    calls so far
    6

    11The last child is "d". Root pauses and calls countFiles("d").

  12. call stack
    0root
    root
    0a
    1[b, c]
    2dchild
    root total
    4
    calls so far
    6

    12"d" is a file and returns 1. Root adds it: its total is 4.

  13. call stack
    root
    0a
    1[b, c]
    2d
    answer
    4
    calls
    6
    tallest stack
    3

    13Root has no children left and returns 4. The stack is empty again. 6 calls in all: one per file or folder. The stack was never more than 3 calls tall.

1 / 13

Every waiting call costs a little memory, because the computer has to remember where it paused. So recursion uses memory in proportion to how deep the calls go, even if you never create a list yourself.

TimeO(n)
SpaceO(d)

n is the number of files and folders: one call each. d is the deepest nesting: that many calls wait on the stack at once.

When recursion repeats itself

Some problems split into pieces that overlap. A classic is the Fibonacci sequence, 0, 1, 1, 2, 3, 5, 8, where each number is the sum of the two before it. The recursive version is one line: fib(n) = fib(n − 1) + fib(n − 2), with fib(0) = 0 and fib(1) = 1 as base cases. It is correct. It is also very slow.

fib(4) fib(3) fib(2) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
The calls made by fib(4): 9 calls to compute one small number. fib(2) is worked out twice from scratch, and fib(1) three times.

Each call that is not a base case makes two more. The tree gets bushier fast: every time n goes up by 1, the number of calls grows by about 1.6 times. fib(4) makes 9 calls. fib(30) makes 2,692,537, almost all of them repeats. The fix is to remember answers you already worked out, which is the starting point of dynamic programming.

ApproachTimeSpaceWhy
Count filesO(n)O(d)Each item is visited once. The pieces never overlap.
Naive FibonacciO(2ⁿ)O(n)Up to two calls per call, and the same values are recomputed many times. The true growth is about 1.6ⁿ; 2ⁿ is the simple upper bound people quote.

Recursion or a loop

Anything recursive can be rewritten with a loop and a list you manage yourself as a stack. Here is how to choose:

  • Use recursion when the data is nested: folders, trees, menus inside menus, JSON inside JSON. The code mirrors the shape of the data.
  • Use recursion when the problem splits into smaller copies, as in merge sort and divide and conquer, or when you try choices one at a time, as in backtracking.
  • Use a loop for a flat walk over a list. Summing 100,000 numbers recursively, one call per number, means 100,000 waiting calls. Python refuses at about 1,000 by default.

Common mistakes

Check yourself

Quick check

How many calls does countFiles([["x"], "y"]) make in total, including the first one?

A recursive sum(nums, i) returns nums[i] + sum(nums, i + 1) and has no base case. What happens on [1, 2, 3]?

A folder structure is 10 levels deep and holds 5,000 files in total. Roughly how many calls of countFiles are waiting on the stack at the busiest moment?

What to practise next

Next, Math for Algorithms covers the few formulas that make costs like these easy to estimate. To practise recursion now, solve Moving the Wedding Cake, where a few lines of recursion solve a puzzle that is awkward to write as a plain loop. Later, Tree Traversals uses exactly the folder pattern, and Backtracking uses it to try every combination.