Part 2 · Lesson 14 · 24 min read

Recursion Basics

A function that calls itself, a base case that stops it, and the call stack underneath.

On this page

  1. A function that calls itself
  2. Base case and recursive case
  3. Traced: factorial(4) and the call stack
  4. The call stack, drawn
  5. Printing 1 to N and N to 1
  6. Sum and factorial
  7. Recursion on arrays and strings
  8. Two calls at once: Fibonacci
  9. Stack overflow
  10. Recursion or a loop?
  11. Practice questions (7)
  12. Make it stick
  13. What's next

A function that calls itself

You are standing in a long queue (a line of people) and want to know your position. You cannot see the front. So you ask the person in front of you: “What is your position?” They do not know either, so they ask the person in front of them, and so on. The person at the very front knows the answer without asking: “1”. Each answer then travels back: 2, 3, 4… until it reaches you.

That is recursion. Recursion is a way of solving a problem by asking the same question about a smaller version of the problem. In code, it is a function that calls itself. (A function is a named block of code that you can run by calling its name.) The example below counts down from 3. Each call prints one number and then calls the same function with a smaller number.

JavaScript
function countdown(n) {  if (n === 0) {                // base case: stop here    console.log("Lift-off!");    return;  }  console.log(n);  countdown(n - 1);             // recursive case: a smaller problem}countdown(3);// 3// 2// 1// Lift-off!

Base case and recursive case

Every correct recursive function has these three parts:

The three rules of recursion
  1. A base case is the smallest input. The function answers it directly, with no further call. (This is the person at the front of the queue.)
  2. A recursive case is the part where the function calls itself on a smaller input and uses that answer.
  3. Progress means every call moves closer to the base case, so the base case is always reached.

The hardest part for beginners is trusting the recursive call. Take the factorial as an example. The factorial of n, written n!, is n × (n − 1) × … × 1. So 4! = 4 × 3 × 2 × 1 = 24. When you write factorial(n - 1), do not try to follow it all the way down. Assume it returns the correct answer for n − 1. Then ask only: how do I use that to get the answer for n? Since n! = n × (n − 1)!, the answer is n * factorial(n - 1).

Traced: factorial(4) and the call stack

Now let us follow it all the way down once, to see what the computer really does. Watch the stack variable. It shows every call that has started but has not finished yet.

step 1 / 16
Set up
Call factorial(4)

The first call. A new frame is placed on the call stack.

▶function factorial(n) {2  if (n === 1) return 1;3  return n * factorial(n - 1);4}5console.log(factorial(4));
Variables
n4
stack["factorial(4)"]
Console
(nothing printed yet)
Going in, each call pauses and waits for a smaller one. Coming out, each paused call resumes and finishes its multiplication. · interactive

The call stack, drawn

The call stack is the list of function calls that are still running. A stack is a list where you add and remove items only at the top, like a pile of plates. Each call gets its own frame. A frame is the note the computer keeps for one call: its own copy of n and the place where the call must continue. New frames go on top. Only the top frame runs. When it returns, it is removed and the frame below continues.

Dry run · the call stack for factorial(4), top of stack on the left
MomentStack (top first)What happens
1f(4)f(4) needs f(3)
2f(3) · f(4)f(3) needs f(2)
3f(2) · f(3) · f(4)f(2) needs f(1)
4f(1) · f(2) · f(3) · f(4)base case: f(1) returns 1
5f(2) · f(3) · f(4)f(2) returns 2 × 1 = 2
6f(3) · f(4)f(3) returns 3 × 2 = 6
7f(4)f(4) returns 4 × 6 = 24

Four frames exist at the deepest point. A recursion that goes n levels deep needs n frames at once. That is O(n) memory, even though we made no array.

This is why recursion has a space cost. Lesson 12 said that space complexity counts the extra memory a program uses. The call stack is extra memory. So factorial(n) uses O(n) space.

Printing 1 to N and N to 1

Where you put the work decides the order. You can put it before the recursive call or after it. Work done before the call happens on the way down. Work done after the call happens on the way back up.

JavaScript
function upTo(n) {  if (n === 0) return;  upTo(n - 1);          // first handle 1 … n-1  console.log(n);       // then print n  → printed on the way back}function downFrom(n) {  if (n === 0) return;  console.log(n);       // print n first → printed on the way in  downFrom(n - 1);}upTo(3);    // 1 2 3downFrom(3); // 3 2 1
Dry run · upTo(3): the print runs after the call returns
Going inComing back out
upTo(3) calls upTo(2)upTo(3) prints 3 (last)
upTo(2) calls upTo(1)upTo(2) prints 2
upTo(1) calls upTo(0)upTo(1) prints 1 (first)
upTo(0) returns (base case)

Sum and factorial

Many maths definitions are already recursive. Write the definition, translate each line into code, and the function is finished:

JavaScript
// sum(n) = n + sum(n - 1),   sum(0) = 0function sumTo(n) {  if (n === 0) return 0;  return n + sumTo(n - 1);}// n! = n × (n - 1)!,   0! = 1function factorial(n) {  if (n <= 1) return 1;  return n * factorial(n - 1);}console.log(sumTo(5));      // 15console.log(factorial(5));  // 120console.log(factorial(0));  // 1

Note the base case n <= 1 in this factorial. It also covers 0! = 1 (by definition, the factorial of 0 is 1). If the base case were only n === 1, then factorial(0) would call factorial(-1), then factorial(-2), and never stop. (The earlier traced version only works for n ≥ 1.)

Recursion on arrays and strings

For an array or string, “a smaller problem” usually means “the same thing without its first item”. You can pass an index that moves forward, or you can pass a shorter copy:

JavaScript
// The sum of an array = first item + the sum of the restfunction sumArray(arr, i = 0) {  if (i === arr.length) return 0;          // nothing left  return arr[i] + sumArray(arr, i + 1);}// A string reversed = (the rest reversed) + the first characterfunction reverse(s) {  if (s.length <= 1) return s;  return reverse(s.slice(1)) + s[0];}console.log(sumArray([4, 1, 7]));  // 12console.log(reverse("code"));      // edoc

Passing an index is cheaper. s.slice(1) makes a copy of the string (everything except the first character) on every call. That adds an O(n) step to each of the n calls, so the total is O(n²). For short inputs either way is fine. In interviews, prefer the index.

Two calls at once: Fibonacci

The Fibonacci numbers are a list of numbers where each one is the sum of the two before it: 0, 1, 1, 2, 3, 5, 8, 13… The definition translates directly into code with two recursive calls:

JavaScript
function fib(n) {  if (n < 2) return n;                 // fib(0) = 0, fib(1) = 1  return fib(n - 1) + fib(n - 2);      // two recursive calls}console.log(fib(10)); // 55

It is correct, but look at how many calls it makes. fib(5) calls fib(4) and fib(3). Then fib(4) calls fib(3) again. The same values are worked out over and over:

Dry run · total calls made by fib(n)
nfib(n)Calls
5515
1055177
206,76521,891
30832,0402,692,537

Each step up in n multiplies the work by about 1.6. That is exponential growth (the work multiplies at every step), roughly O(2ⁿ) from Lesson 12.

A loop that remembers the last two values does the same job in O(n). Lesson 32 draws these calls as a tree. Lesson 54 fixes the repetition with memoisation. Memoisation means saving the answer of a call the first time you work it out, so that you can reuse it instead of calculating it again. It is the first step into dynamic programming (solving a big problem by saving the answers to its small problems).

Stack overflow

Every frame uses memory, and the call stack has a size limit. Suppose there is no base case, or the base case is never reached. Then the stack fills up. This is called a stack overflow, and JavaScript stops the program with an error:

JavaScript
function forever(n) {  return forever(n + 1);   // no base case}forever(0);// RangeError: Maximum call stack size exceeded

In Node.js the limit is roughly 10,000 frames for a simple function. The exact number depends on the engine and on how much each frame stores. So even a correct recursion fails if it goes too deep. sumTo(100000) overflows, while a loop handles it easily.

When you see “Maximum call stack size exceeded”

Check three things. Is there a base case? Does every call move towards it? Could the input make the recursion deeper than about 10,000 levels?

Recursion or a loop?

Anything written with recursion can be written with a loop, and the other way round. Choose the one that is easier to read:

Use a loop when…Use recursion when…
You walk through items one by one (sum, max, count)The problem splits into smaller copies of itself (trees, merge sort)
The depth could be very large (100,000+)The depth is small, such as log n or the height of a tree
You want O(1) extra spaceYou need to try several choices and undo them (backtracking)

For the simple problems in this lesson, loops are usually better in practice. We use recursion here because the problems are small enough to follow every step. You will need the same skill for trees (Part 11), backtracking (Part 8) and dynamic programming (Part 14), where recursion is the natural tool. (Backtracking means trying a choice, and going back to undo it if it does not work.)

Practice questions

For each question, write the base case first, then the recursive case. Then dry-run the smallest input that is not the base case. (To dry-run means to follow the code by hand, line by line, with a small input.)

Q1Sum of digits, recursivelyEasy

Return the sum of the digits of a non-negative integer n, using recursion.

Example 1
Input: 472
Output: 13
Explanation: 4 + 7 + 2 = 13.
Example 2
Input: 5
Output: 5
Explanation: A single digit is its own sum. This is the base case.
Stuck? Open one hint at a time
Hint 1
Smaller problem: the number without its last digit, Math.floor(n / 10).
Hint 2
sumDigits(n) = (last digit) + sumDigits(rest).
Show all 2 solutions with explanations
Approach 1Recursion
The idea

The answer is the last digit plus the digit sum of the rest. A number below 10 has only one digit, so it is its own answer.

JavaScript
function sumDigits(n) {  if (n < 10) return n;  return (n % 10) + sumDigits(Math.floor(n / 10));}console.log(sumDigits(472)); // 13console.log(sumDigits(5));   // 5
How it works
Dry run · sumDigits(472)
CallReturns
sumDigits(472)2 + sumDigits(47) = 2 + 11 = 13
sumDigits(47)7 + sumDigits(4) = 7 + 4 = 11
sumDigits(4)4 (base case)
Approach 2Loop
The idea

The digit loop from Lesson 5.

JavaScript
function sumDigits(n) {  let sum = 0;  while (n > 0) {    sum += n % 10;    n = Math.floor(n / 10);  }  return sum;}console.log(sumDigits(472)); // 13
How it works

The time is the same, O(log n). The space is O(1) instead of O(log n) stack frames.

Which approach should you use?

Both are fine. The depth is only the number of digits. The recursion is a good first exercise because the smaller problem is easy to see.

Q2Power of twoEasy

A power of two is 1, 2, 4, 8, 16 and so on (2 multiplied by itself some number of times). Return true if n is a power of two.

Example 1
Input: 16
Output: true
Explanation: 16 = 2⁴.
Example 2
Input: 1
Output: true
Explanation: 1 = 2⁰. This is the base case.
Example 3
Input: 6
Output: false
Explanation: 6 / 2 = 3, and 3 is odd and not 1.
Example 4
Input: 0
Output: false
Explanation: Edge case: 0 is not a power of two (and halving it never ends).
Stuck? Open one hint at a time
Hint 1
n is a power of two exactly when n is 1, or n is even and n / 2 is a power of two.
Hint 2
Do not forget n ≤ 0.
Show all 2 solutions with explanations
Approach 1Recursion
The idea

Turn the definition into code. Use base cases for 1 and for any number that is not positive or is odd. Otherwise call the function on n / 2.

JavaScript
function isPowerOfTwo(n) {  if (n === 1) return true;  if (n <= 0 || n % 2 !== 0) return false;  return isPowerOfTwo(n / 2);}console.log(isPowerOfTwo(16)); // trueconsole.log(isPowerOfTwo(1));  // trueconsole.log(isPowerOfTwo(6));  // falseconsole.log(isPowerOfTwo(0));  // false
How it works

O(log n) calls, because n halves each time. Without the n <= 0 check, isPowerOfTwo(0) would recurse on 0 forever and overflow the stack.

Approach 2Loop
The idea

Keep dividing by 2 while n is even. A power of two ends at exactly 1.

JavaScript
function isPowerOfTwo(n) {  if (n <= 0) return false;  while (n % 2 === 0) n /= 2;  return n === 1;}console.log(isPowerOfTwo(16)); // trueconsole.log(isPowerOfTwo(6));  // false
How it works

The time is the same, and the space is O(1). (Lesson 58 shows a one-line bit trick for this.)

Which approach should you use?

Notice that the recursive version has several base cases. That is normal. List every input you can answer directly. (LeetCode 231.)

Q3Reverse a string in placeEasy

Reverse an array of characters in place. “In place” means you change the same array and do not make a new one.

Example 1
Input: ["h", "e", "l", "l", "o"]
Output: ["o", "l", "l", "e", "h"]
Explanation: The characters are swapped from both ends towards the middle.
Stuck? Open one hint at a time
Hint 1
Swap the first and last characters. What smaller problem is left?
Hint 2
Call the function again with left + 1 and right - 1. Stop when they meet.
Show all 2 solutions with explanations
Approach 1Recursion with two indices
The idea

Swap s[left] and s[right], then reverse the part between them.

JavaScript
function reverseString(s, left = 0, right = s.length - 1) {  if (left >= right) return;               // 0 or 1 characters left  [s[left], s[right]] = [s[right], s[left]];  reverseString(s, left + 1, right - 1);}const s = ["h", "e", "l", "l", "o"];reverseString(s);console.log(s); // [ 'o', 'l', 'l', 'e', 'h' ]
How it works

It makes about n/2 calls, so the time is O(n) and the stack space is O(n). Default parameters (values used when the caller gives none) let the caller write just reverseString(s). The line with [s[left], s[right]] = [s[right], s[left]] swaps two items at once.

Approach 2Two-pointer loop
The idea

The same swaps in a while loop.

JavaScript
function reverseString(s) {  let left = 0, right = s.length - 1;  while (left < right) {    [s[left], s[right]] = [s[right], s[left]];    left++;    right--;  }}const s = ["h", "e", "l", "l", "o"];reverseString(s);console.log(s); // [ 'o', 'l', 'l', 'e', 'h' ]
How it works

O(n) time and O(1) space. This is the version that LeetCode's “O(1) extra memory” rule asks for.

Which approach should you use?

Compare the two. The loop's left++ and right-- became the arguments of the recursive call. Changing a loop into recursion (or back) is often this direct. (LeetCode 344.)

Q4Palindrome check, recursivelyEasy

A palindrome is a word that reads the same forwards and backwards, like “racecar”. Return whether the string s is a palindrome, using recursion.

Example 1
Input: "racecar"
Output: true
Explanation: r = r, a = a, c = c, then only e is left.
Example 2
Input: "abca"
Output: false
Explanation: The outer pair matches, but b ≠ c.
Example 3
Input: ""
Output: true
Explanation: Edge case: an empty string reads the same both ways.
Stuck? Open one hint at a time
Hint 1
A string is a palindrome if its first and last characters match and the middle part is a palindrome.
Show the solution with explanations
Approach 1Recursion with two indices
The idea

Compare the two ends. If they match, check the inside.

JavaScript
function isPalindrome(s, i = 0, j = s.length - 1) {  if (i >= j) return true;  if (s[i] !== s[j]) return false;  return isPalindrome(s, i + 1, j - 1);}console.log(isPalindrome("racecar")); // trueconsole.log(isPalindrome("abca"));    // falseconsole.log(isPalindrome(""));        // true
How it works

Two base cases: nothing left to compare (true), or a mismatch (false). O(n) time, O(n) stack.

Which approach should you use?

Lesson 9 solved this with a loop. This version shows the same idea as a definition. Both are O(n) time.

Q5Fibonacci numberEasy

Return the n-th Fibonacci number, where fib(0) = 0 and fib(1) = 1 (0 ≤ n ≤ 30).

Example 1
Input: 2
Output: 1
Explanation: 0, 1, 1 — fib(2) = 1.
Example 2
Input: 10
Output: 55
Explanation: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55.
Stuck? Open one hint at a time
Hint 1
The direct recursion is correct, but it repeats work. For n ≤ 30 it is still fast enough.
Hint 2
A loop only needs the last two values.
Show all 2 solutions with explanations
Approach 1Direct recursion
The idea

fib(n) = fib(n − 1) + fib(n − 2), with fib(0) = 0 and fib(1) = 1.

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

The time is about O(2ⁿ), because the same values are worked out many times. The stack depth is O(n).

Approach 2Loop with two variables
The idea

Walk forward from 0 and 1, keeping only the previous two numbers.

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(2));  // 1console.log(fib(10)); // 55console.log(fib(50)); // 12586269025
How it works

O(n) time and O(1) space. fib(50) is instant here. The recursive version would make over 40 billion calls.

Which approach should you use?

Know both. The recursion shows that you understand the definition. The loop shows that you noticed the repeated work. Lesson 54 adds the middle way: recursion that remembers its answers. (LeetCode 509.)

Q6Is the array sorted?Easy

Return whether the array is sorted in non-decreasing order (each item is equal to or bigger than the one before it), using recursion.

Example 1
Input: [1, 2, 2, 5]
Output: true
Explanation: Every item is at least the one before it.
Example 2
Input: [3, 1, 4]
Output: false
Explanation: 1 is smaller than 3.
Example 3
Input: []
Output: true
Explanation: Edge case: nothing is out of order.
Stuck? Open one hint at a time
Hint 1
The array from index i onwards is sorted if arr[i] <= arr[i + 1] and the array from i + 1 onwards is sorted.
Show the solution with explanations
Approach 1Recursion with an index
The idea

Check one pair of neighbours, then check the rest.

JavaScript
function isSorted(arr, i = 0) {  if (i >= arr.length - 1) return true;     // 0 or 1 items left  if (arr[i] > arr[i + 1]) return false;  return isSorted(arr, i + 1);}console.log(isSorted([1, 2, 2, 5])); // trueconsole.log(isSorted([3, 1, 4]));    // falseconsole.log(isSorted([]));           // true
How it works

O(n) time and O(n) stack. Note that the base case uses arr.length - 1, because each step looks at i + 1.

Which approach should you use?

The pattern “check the first part, then call the function on the rest” works for any question of the form “do all items satisfy a rule?”.

Q7Count occurrencesEasy

Return how many times x appears in arr, using recursion.

Example 1
Input: arr = [3, 1, 3, 3, 2], x = 3
Output: 3
Explanation: 3 appears at indices 0, 2 and 3.
Example 2
Input: arr = [], x = 7
Output: 0
Explanation: Edge case: empty array.
Stuck? Open one hint at a time
Hint 1
count(from i) = (1 if arr[i] is x, else 0) + count(from i + 1).
Show all 2 solutions with explanations
Approach 1Return-value recursion
The idea

Each call returns the count for its part of the array. The caller then adds its own item.

JavaScript
function countOf(arr, x, i = 0) {  if (i === arr.length) return 0;  return (arr[i] === x ? 1 : 0) + countOf(arr, x, i + 1);}console.log(countOf([3, 1, 3, 3, 2], 3)); // 3console.log(countOf([], 7));              // 0
How it works

The answer is built on the way back up. The deepest call returns 0, and each frame adds 0 or 1.

Approach 2Parameter recursion
The idea

Carry the count so far as an argument, and return it at the end.

JavaScript
function countOf(arr, x, i = 0, count = 0) {  if (i === arr.length) return count;  return countOf(arr, x, i + 1, count + (arr[i] === x ? 1 : 0));}console.log(countOf([3, 1, 3, 3, 2], 3)); // 3
How it works

Here the answer is built on the way down. The count works like an accumulator (a variable that collects a running total) in a loop. The base case simply returns it.

Which approach should you use?

These are the two styles of recursion that you will meet again in Lesson 32: build the answer from returned values, or carry it in a parameter. Both are O(n).

Make it stick

Make it stick — close this page, then
  1. State the three rules of recursion from memory.
  2. Draw the call stack for sumTo(3) at its deepest point, with what each frame is waiting for.
  3. Explain why upTo(n) prints 1 first even though it is called with n first.
  4. Say why fib(30) makes millions of calls, and what the loop version does instead.

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

Lesson 15 closes Part 2 with hashing. You will count with arrays and Maps and answer frequency questions. Then you will look inside a hash table to see why map.get takes about the same time for ten items as for ten million.

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