A Little Introduction to Recursion

por Frank de Alcantara em 08/09/2024

A Little Introduction to Recursion

Recursion lets us solve a problem by assuming we can already solve smaller versions of it. This guide builds the idea from its mathematical root in induction through the call stack, the recursion tree, tail recursion, mutual recursion, divide-and-conquer, and the memoization that rescues recursion from its own redundant work. Interactive labs let you watch every one of these mechanisms run.

Recursion is a way of thinking before it is a programming technique. It rests on a single, almost impudent, act of faith: to solve a problem, assume you can already solve a smaller version of the same problem, and figure out only how to bridge the gap between the two. The attentive reader will notice the circularity and be right to be suspicious. This article is, in large part, an argument that the circularity is not vicious. It is disciplined by a base case, licensed by mathematical induction, and made concrete by what happens when a function calls itself.

We start where the idea itself started, in mathematical logic, and show that recursion is induction read backwards. We then descend from the abstract to the concrete: the call stack that actually executes a recursive function, the recursion tree that explains why the naive Fibonacci is catastrophically slow, and the memoization that fixes it. Along the way we meet tail recursion, which turns a recursive call into a loop, mutual recursion, in which two functions call each other, and divide-and-conquer, the strategy behind the fastest sorting algorithms. We close with the pitfalls that turn an elegant recursive idea into a program that crashes. Every mechanism is paired with an interactive lab, because recursion is far easier to see than to describe.

1. Where recursion comes from

Recursion did not begin in a programming language, because there were none. It emerged from mathematical logic in the 1930s, during the effort to make precise what it means for a function to be computable at all. Kurt Gödel helped formalize general recursive functions; Alonzo Church developed the lambda calculus, a formal system in which computation proceeds through function abstraction, application, and substitution. Recursion can be expressed in the untyped lambda calculus through fixed-point constructions. General recursive functions, lambda calculus, and Turing machines were shown to characterize the same class of computable functions, and the Church–Turing thesis connects these formal models to the intuitive notion of effective mechanical computation. Recursion, in other words, is not merely one programming technique among many; it belongs to the foundations of computation.

The idea reached programming through influential early languages such as Lisp in the late 1950s, which treated recursion as a natural way to define functions rather than as an exotic special case. From Lisp the idea spread widely, and in modern functional languages such as Haskell it is not merely available but central. Haskell has no imperative loop statement; repetition is expressed through recursion or abstractions such as folds that are themselves defined recursively. When we later write a recursive definition in C++, the reader should remember that in another family of languages that definition would not be a stylistic choice but the natural foundation for iteration.

2. Recursion is induction, read backwards

The mathematical backbone of recursion is the principle of mathematical induction, and the resemblance is not an analogy but an identity of structure. Induction proves that a statement holds for every natural number by proving two things: that it holds for a smallest case, and that whenever it holds for some case it holds for the next. Recursion computes a value for every natural number by the same two-part move: it gives the answer outright for a smallest case, and it reduces every larger case to a smaller one.

Every recursive definition therefore has exactly two parts, and both are mandatory. The base case is the smallest instance, whose answer is given directly and which stops the recursion. The recursive step expresses the answer for a general instance in terms of strictly smaller instances, and it is this word strictly that guarantees progress: each call must move measurably closer to the base case, or the recursion never ends. A recursive definition that reduces $n$ to $n-1$ terminates because $n-1$ is closer to zero; one that reduces $n$ to $n$, or to $n+1$, does not.

Consider the factorial, the canonical first example, defined by

\[\text{factorial}(n) = \begin{cases} 1 & \text{if } n = 0 \\ n \times \text{factorial}(n-1) & \text{if } n > 0. \end{cases}\]

Here the base case is $n = 0$, whose value is $1$ by definition, and the recursive step reduces $n$ to $n-1$, which is strictly smaller and non-negative, so the descent reaches $0$ after exactly $n$ steps. The reader who wants to be certain this definition is correct, and not merely terminating, must prove it, and the proof is an induction that mirrors the definition line for line.

We claim that $\text{factorial}(n) = n!$ for all $n \geq 0$. For the base case, $\text{factorial}(0)$ returns $1$, and $0! = 1$ by definition, so the claim holds at $n = 0$. For the inductive step, assume as the inductive hypothesis that $\text{factorial}(k) = k!$ for some $k \geq 0$. Then, by the recursive step, $\text{factorial}(k+1) = (k+1) \times \text{factorial}(k) = (k+1) \times k! = (k+1)!$, where the middle equality is exactly the inductive hypothesis. The claim therefore holds at $k+1$, and by induction it holds for all $n \geq 0$. The structure of the proof is not similar to the structure of the function; it is the same structure. This is why induction is the natural tool for reasoning about recursion, and why a programmer who is fluent in one is already halfway fluent in the other.

3. The anatomy of a recursive function

Before writing any code, it is worth naming the three things that every correct recursive function must have, because the absence of any one of them is the source of most recursion bugs, as we will see in the closing section. It must have at least one base case that returns without recursing. It must have a recursive step that calls the function on a smaller input. And the sequence of inputs generated by repeated recursive calls must be guaranteed to reach a base case in finitely many steps. The factorial has all three: the base case at $0$, the step to $n-1$, and the guarantee that counting down from any $n \geq 0$ reaches $0$.

There is a deep and useful way to picture this, sometimes called the recursive leap of faith. When we write the recursive step, we do not trace what happens inside the smaller call; we simply trust that it returns the correct answer for its smaller input, and we concern ourselves only with combining that answer correctly. In the factorial, we trust that $\text{factorial}(n-1)$ returns $(n-1)!$, and our only job is to multiply by $n$. This is precisely the inductive hypothesis wearing work clothes. It is what makes recursive code short: the programmer specifies one level of the problem and delegates the rest to a copy of herself.

That delegation, however, is not free, and the price is paid in a data structure the programmer rarely sees but always uses. To understand the cost of recursion, which is where the engineering becomes interesting, we must look at the machinery that makes a function call itself: the call stack.

4. The call stack: how recursion actually runs

A running program keeps a region of memory called the call stack, and every time a function is invoked the machine pushes onto it a stack frame holding that call’s arguments, its local variables, and the address to return to when it finishes. When the function returns, its frame is popped and control resumes in the caller. This is true of all function calls, recursive or not; recursion is simply the case in which many frames of the same function are alive on the stack at once, each frozen mid-computation, waiting for the call it made to come back.

Let us trace factorial(4) frame by frame. The first call cannot finish, because to return $4 \times \text{factorial}(3)$ it must first know $\text{factorial}(3)$, so it pushes a frame for factorial(3) and suspends. That call likewise suspends on factorial(2), which suspends on factorial(1), which suspends on factorial(0). At this moment the stack holds five frames, deepest to shallowest:

\[\underbrace{\texttt{factorial(0)}}_{\text{top, base case}} \;\to\; \texttt{factorial(1)} \;\to\; \texttt{factorial(2)} \;\to\; \texttt{factorial(3)} \;\to\; \underbrace{\texttt{factorial(4)}}_{\text{bottom}}\]

Only now does anything return. The base case factorial(0) returns $1$; its frame is popped and factorial(1) computes $1 \times 1 = 1$ and pops; factorial(2) computes $2 \times 1 = 2$ and pops; factorial(3) computes $3 \times 2 = 6$; and finally factorial(4) computes $4 \times 6 = 24$. The recursion is thus two motions, not one: a winding phase that pushes frames all the way down to the base case, and an unwinding phase that pops them, computing the actual multiplications on the way back up. Nothing is multiplied until the descent hits bottom.

This picture explains the space cost of recursion exactly. The stack reaches its maximum height when the base case is reached, and that height equals the depth of the recursion. For factorial(n) the depth is $n+1$ frames, so the space complexity is $\Theta(n)$, whether or not the function’s result itself is small. It also explains a failure mode that iteration does not have. If the depth grows large enough, perhaps tens or hundreds of thousands of frames depending on the language, compiler, and platform, the stack runs out of room and the program dies with a stack overflow, a pitfall we return to in Section 11.

The following lab lets the reader run this descent and ascent one step at a time. Push frames until the base case appears, then pop them and watch each multiplication resolve. Set $n$ to a larger value to feel how the stack height grows in lockstep with $n$.

In C++ the factorial is a direct transcription of the definition, and it runs exactly the descent-and-ascent just described. The largest factorial representable by std::uint64_t is $20!$; $21!$ already exceeds the type’s maximum value. The guard in the example makes this numeric boundary explicit instead of allowing silent unsigned overflow. We compile the examples in this article with g++ -O2 -std=c++23 -Wall -Wextra -Wpedantic.

#include <cstdint>
#include <stdexcept>

constexpr std::uint64_t factorial(std::uint64_t n) {
    if (n > 20) {
        throw std::overflow_error("factorial exceeds std::uint64_t");
    }
    if (n == 0) return 1;          // The base case terminates the descent.
    return n * factorial(n - 1);   // n - 1 is strictly closer to the base case.
}

static_assert(factorial(0) == 1);
static_assert(factorial(4) == 24);

The multiplication n * factorial(n - 1) is the last expression in the function, but it is not the last operation performed on this call’s behalf. The call to factorial(n - 1) must return before the multiplication can run, so the frame must stay alive across the recursive call. That detail, easy to miss, is exactly what separates ordinary recursion from the tail recursion of Section 8. This version needs $\Theta(n)$ stack frames; a tail-recursive version can use one frame only when the compiler actually performs tail-call optimisation.

5. When recursion multiplies: the recursion tree

The factorial makes exactly one recursive call per invocation, so its call structure is a straight line of depth $n$. Many problems are not so kind. The Fibonacci sequence, defined by $F(0) = 0$, $F(1) = 1$, and $F(n) = F(n-1) + F(n-2)$ for $n \geq 2$, is recursive by definition because each term depends on the two before it. Its literal transcription makes two recursive calls per invocation. That single change transforms the linear chain of the factorial into a branching tree, and the consequences are severe.

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

Code Fragment 1: the naive recursive Fibonacci, shown in Python as executable pseudocode.

The code is a flawless mirror of the mathematics, and for that it deserves credit; it is also, for large $n$, nearly unusable, and understanding precisely why is the most instructive thing in this article. Let us compute $F(5)$ by hand to see the structure. The call fibonacci(5) needs fibonacci(4) and fibonacci(3); fibonacci(4) needs fibonacci(3) and fibonacci(2); and so on, until every branch bottoms out at $F(1) = 1$ or $F(0) = 0$. Reading the returns back up the tree gives $F(2) = 1$, $F(3) = 2$, $F(4) = 3$, and finally $F(5) = 3 + 2 = 5$. The answer is correct. The problem is everything the machine did to get there.

5.1 Counting the calls

The attentive reader will already have spotted the waste: to compute fibonacci(5) the function computes fibonacci(3) twice, fibonacci(2) three times, and fibonacci(1) five times. Each is computed from scratch, oblivious that the very same value was just computed on a neighbouring branch. Enumerating every node of the call tree for $F(5)$ and tallying them by argument gives:

Call Times invoked
fibonacci(5) 1
fibonacci(4) 1
fibonacci(3) 2
fibonacci(2) 3
fibonacci(1) 5
fibonacci(0) 3
Total 15

So fibonacci(5) triggers $15$ calls in all. To count calls for general $n$ without drawing the tree, let $T(n)$ be the total number of invocations needed to compute fibonacci(n). Every call to fibonacci(n) performs one invocation of itself and then triggers all the invocations of its two children, giving the recurrence

\[T(n) = T(n-1) + T(n-2) + 1, \qquad T(0) = T(1) = 1,\]

where the $+1$ counts the current call and the two terms count its subtrees. Unrolling it from the base cases produces $T(2) = 3$, $T(3) = 5$, $T(4) = 9$, and $T(5) = 15$, matching our hand count. Continuing gives $T(10) = 177$. The call-counting recurrence is itself a Fibonacci-shaped recurrence, which hints at a closed form. Indeed, one can prove by induction that

\[T(n) = 2F(n+1) - 1,\]

and the reader can check it immediately: $T(5) = 2F(6) - 1 = 2 \cdot 8 - 1 = 15$, and $T(10) = 2F(11) - 1 = 2 \cdot 89 - 1 = 177$. The number of calls grows like the Fibonacci numbers themselves, which is the whole trouble, as the next section makes quantitative.

The lab below draws the recursion tree for any $n$ up to a manageable size, colours every node by how many times its value is recomputed, and reports $T(n)$ live. Increase $n$ by one and watch the tree, together with the redundant work highlighted in it, grow by a factor that approaches $\varphi$.

6. The cost of naive recursion

We can now pin down the complexity of Code Fragment 1 with more care than the usual hand-wave. A common first estimate reasons that the call tree branches in two at every level down to depth $n$, giving at most $2^n$ nodes and therefore $O(2^n)$ time. That bound is correct but loose, because the two subtrees are not the same size: the $F(n-2)$ branch is shorter than the $F(n-1)$ branch, so the tree is lopsided and has fewer than $2^n$ nodes.

The exact growth follows from the closed form $T(n) = 2F(n+1) - 1$ of the previous section together with the well-known closed form of the Fibonacci numbers themselves,

\[F(n) = \frac{\varphi^n - \psi^n}{\sqrt{5}}, \qquad \varphi = \frac{1 + \sqrt{5}}{2} \approx 1.618, \quad \psi = \frac{1 - \sqrt{5}}{2} \approx -0.618,\]

in which $\varphi$ is the golden ratio. Since $\lvert \psi \rvert < 1$, the $\psi^n$ term vanishes for large $n$, so $F(n) \approx \varphi^n / \sqrt{5}$ and therefore $T(n) = \Theta(\varphi^n)$. The number of calls is exponential, but in base $\varphi \approx 1.618$, not base $2$. Numerically the difference is enormous: at $n = 30$ the loose bound $2^n$ exceeds one billion, whereas the true count $T(30) = 2\,692\,537$ is smaller by a factor of roughly four hundred. Exponential is exponential, and either way the function is hopeless for, say, $n = 60$; but a correct analysis says $\Theta(\varphi^n)$, and the reader should distrust any source that stops at $2^n$ without noting that it overcounts.

The space complexity, by contrast, is gentle. However wide the recursion tree gets, only one root-to-leaf path is ever on the call stack at a time. The machine fully finishes one subtree and pops its frames before evaluating the other. The deepest such path has length proportional to $n$, so the stack holds $\Theta(n)$ frames at its peak. The naive Fibonacci is thus cheap in memory compared with its ruinous running time, and the ruin comes entirely from recomputing values it already knew. That single sentence is the doorway to dynamic programming.

7. Memoization and dynamic programming

Recursion and dynamic programming touch here, and the meeting is not gentle. Both split a problem into subproblems; the difference is bookkeeping. Plain recursion solves each subproblem afresh every time it is asked, however many times that is. Dynamic programming solves each distinct subproblem once, stores the answer, and returns the stored answer on every later request. The naive Fibonacci recomputes fibonacci(3) twice and fibonacci(1) five times; dynamic programming computes each of $F(0)$ through $F(n)$ exactly once. The observation that unlocks everything is that although the call tree has $\Theta(\varphi^n)$ nodes, it contains only $n+1$ distinct subproblems, namely $F(0), F(1), \dots, F(n)$. The exponential blow-up is entirely duplication.

The gentlest way to apply this keeps the recursive code and adds a cache, a technique called memoization. Before computing a value we check whether it is already in a table; if so we return it, and if not we compute it recursively and store it before returning. Each of the $n+1$ subproblems is then computed once and thereafter merely looked up.

#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <vector>

static std::uint64_t fib_memo_impl(
    std::size_t n,
    std::vector<std::uint64_t>& memo
) {
    if (n <= 1) return n;
    if (memo[n] != 0) return memo[n];  // For n >= 2, zero means not computed.

    // Name both dependencies to make their evaluation order explicit.
    const std::uint64_t previous = fib_memo_impl(n - 1, memo);
    const std::uint64_t before_previous = fib_memo_impl(n - 2, memo);
    memo[n] = previous + before_previous;
    return memo[n];
}

std::uint64_t fib_memo(std::size_t n) {
    if (n > 93) {
        throw std::overflow_error("F(n) exceeds std::uint64_t");
    }
    std::vector<std::uint64_t> memo(n + 1, 0);
    return fib_memo_impl(n, memo);
}

This is called top-down dynamic programming because it still starts from $F(n)$ and recurses downward. Now each distinct subproblem performs real work only once, and later requests become cache hits. Naming previous before before_previous also fixes their evaluation order, something a single C++ addition expression would not guarantee. After fib_memo_impl(n - 1, memo) returns, the value for $n-2$ is already cached. The recurrence for the work that computes new values is no longer $T(n) = T(n-1) + T(n-2) + 1$ but

\[T(n) = T(n-1) + O(1),\]

because fib_memo_impl(n - 2, memo) was already resolved while computing fib_memo_impl(n - 1, memo) and now costs a constant lookup rather than a whole subtree. This solves to $T(n) = O(n)$: we have traded exponential time for linear time at the cost of a table of $n+1$ entries, which is $O(n)$ extra space. The public wrapper also enforces $n \leq 93$, the largest Fibonacci index whose value fits in std::uint64_t.

The same idea, run in the other direction, is bottom-up dynamic programming: rather than recurse from $F(n)$ down, we build the values from $F(0)$ up, and no recursion or recursive call stack is used at all.

#include <cstddef>
#include <cstdint>
#include <stdexcept>

constexpr std::uint64_t fib_bottom_up(std::size_t n) {
    if (n > 93) {
        throw std::overflow_error("F(n) exceeds std::uint64_t");
    }
    if (n <= 1) return n;
    std::uint64_t prev = 0;
    std::uint64_t curr = 1;
    for (std::size_t i = 2; i <= n; ++i) {
        const std::uint64_t next = prev + curr;  // Both operands fit for i <= 93.
        prev = curr;
        curr = next;
    }
    return curr;
}

static_assert(fib_bottom_up(0) == 0);
static_assert(fib_bottom_up(10) == 55);

The bottom-up version keeps only the two most recent values instead of the whole table, so it runs in $O(n)$ time and $O(1)$ space, and it never risks a stack overflow because it never recurses. This is the recurring lesson of the relationship: recursion is often the clearest way to state a solution, and dynamic programming is often the right way to run it. Memoization is the bridge between them, recursive in form and linear in cost.

The lab below compares the total calls made by naive recursion with the $n+1$ distinct subproblems that perform real work in the memoized version. Cache hits still invoke the helper, but they return in constant time and do not recompute a subtree. The gap between $\Theta(\varphi^n)$ and $O(n)$ is abstract on the page and visceral once the two counters are racing.

8. Tail recursion: recursion that costs no stack

Section 4 pointed to a subtle feature of factorial: the multiplication n * factorial(n - 1) happens after the recursive call returns, so each frame must survive across its recursive call, and the stack grows to depth $n$. A recursive call is in tail position when it is the very last action of the function. The function returns the result of that call directly, with no pending work left afterwards. A function whose recursive calls are all in tail position is tail-recursive, and it can, in principle, run in constant stack space.

The trick that puts the call in tail position is an accumulator: an extra parameter that carries the work-so-far forward into the recursion, so that nothing remains to be done on the way back. Compare the ordinary factorial with its tail-recursive cousin.

#include <cstdint>
#include <stdexcept>

// Not tail-recursive: a multiplication waits for the call to return.
constexpr std::uint64_t factorial(std::uint64_t n) {
    if (n > 20) {
        throw std::overflow_error("factorial exceeds std::uint64_t");
    }
    if (n == 0) return 1;
    return n * factorial(n - 1);
}

// The helper is tail-recursive: no operation waits after the recursive call.
static constexpr std::uint64_t factorial_tail_impl(
    std::uint64_t n,
    std::uint64_t acc
) {
    if (n == 0) return acc;
    return factorial_tail_impl(n - 1, acc * n);
}

constexpr std::uint64_t factorial_tail(std::uint64_t n) {
    if (n > 20) {
        throw std::overflow_error("factorial exceeds std::uint64_t");
    }
    return factorial_tail_impl(n, 1);
}

static_assert(factorial(4) == factorial_tail(4));

In factorial_tail_impl, the product is computed before the recursive call and passed down in acc, so when the base case is finally reached the answer is already sitting in the accumulator. Tracing factorial_tail(4) shows the accumulator doing the work on the way down. It takes the values $1, 4, 12, 24, 24$, while the ordinary version performs its multiplications on the way up. Because no frame has pending work after its recursive call, a compiler is free to reuse the current frame for the next call instead of pushing a new one. This transformation is called tail-call optimisation. Where it applies, it collapses the $\Theta(n)$ stack of a recursive function to $\Theta(1)$, turning the recursion into a loop in all but name.

Two cautions keep this from being a free lunch. First, some language specifications guarantee tail-call optimisation in relevant cases, especially in functional languages where it supports iteration. C++ does not require it. GCC, Clang, and other optimizing compilers may perform the transformation when the call is eligible and optimization settings permit it, but portable C++ code must not assume that tail recursion uses constant stack space. Second, the transformation helps only when the call is genuinely in tail position. The branching Fibonacci from Section 5 is not tail-recursive because one recursive result must still be combined with another. Tail recursion is a bridge from recursion back to iteration. The lab below models both factorials and, for the tail-recursive version, explicitly assumes that tail-call optimisation has been applied.

9. Mutual recursion: functions that call each other

Recursion does not require a function to call itself directly; it requires only that a call eventually leads back to the same computation. In mutual recursion, two or more functions call one another in a cycle, and the recursion lives in the cycle rather than in any single function. The textbook example decides parity without arithmetic: a number is even if its predecessor is odd, and odd if its predecessor is even, with zero even by definition.

#include <cstdint>

bool is_odd(std::uint64_t n);  // The declaration breaks the definition cycle.

bool is_even(std::uint64_t n) {
    if (n == 0) return true;
    return is_odd(n - 1);
}

bool is_odd(std::uint64_t n) {
    if (n == 0) return false;
    return is_even(n - 1);
}

A call to is_even(4) becomes is_odd(3), then is_even(2), then is_odd(1), then is_even(0), which returns true. The unsigned parameter states that the domain is non-negative. The two functions hand the problem back and forth, each shrinking $n$ by one, and the shared base case at $0$ guarantees termination by exactly the argument of Section 2. The C++ detail worth noting is the forward declaration of is_odd before is_even: because each function names the other, one of them must be announced to the compiler before it is defined, or the first reference will not compile. Mutual recursion is more than a curiosity. Recursive-descent parsers, in which an expression rule calls a term rule, which calls a factor rule, which may call back to the expression rule, are mutual recursion at industrial scale. Their correctness is analysed with the same base-case-and-progress reasoning as ordinary recursion.

10. Divide and conquer: recursion as strategy

The most productive use of recursion is not to walk a sequence one step at a time but to split a problem into a few substantial pieces, solve each recursively, and combine the results. This is divide and conquer, and it is the strategy behind several efficient general-purpose sorting algorithms. Since Julius Caesar it has been easier to divide and conquer; the algorithmic version simply makes the division recursive.

Take merge sort. To sort an array, split it into two halves, sort each half recursively, and merge the two sorted halves into one. The base case is an array of length $0$ or $1$, which is already sorted and stops the recursion. The implementation below is a clarity-first reference version. It creates sub-vectors at each split so that the recursive structure remains visible; production implementations commonly reuse an auxiliary buffer to reduce allocations and copying.

#include <cstddef>
#include <vector>

std::vector<int> merge(const std::vector<int>& a, const std::vector<int>& b) {
    std::vector<int> out;
    out.reserve(a.size() + b.size());
    std::size_t i = 0, j = 0;
    while (i < a.size() && j < b.size())
        out.push_back(a[i] <= b[j] ? a[i++] : b[j++]);
    while (i < a.size()) out.push_back(a[i++]);
    while (j < b.size()) out.push_back(b[j++]);
    return out;
}

std::vector<int> merge_sort(const std::vector<int>& v) {
    if (v.size() <= 1) return v;  // A range of zero or one element is sorted.
    const std::size_t mid = v.size() / 2;
    std::vector<int> left(v.begin(), v.begin() + mid);
    std::vector<int> right(v.begin() + mid, v.end());
    return merge(merge_sort(left), merge_sort(right));  // Combine sorted halves.
}

The cost of a divide-and-conquer algorithm is captured by a recurrence that reflects its structure. Merge sort makes two recursive calls on inputs of half the size and does $\Theta(n)$ work to merge, so

\[T(n) = 2\,T\!\left(\tfrac{n}{2}\right) + \Theta(n).\]

The tree of this recurrence has $\log_2 n$ levels because the input is halved until it reaches size $1$. Every merge level processes $\Theta(n)$ elements in total, since the subarrays at that level collectively contain all $n$ elements. Multiplying the per-level work by the number of levels gives $\Theta(n \log n)$, the hallmark of efficient divide-and-conquer sorting and a decisive improvement over the $\Theta(n^2)$ of simple comparison sorts such as insertion sort in its worst case. For $n = 8$, there are three merge levels and each level moves eight elements into output vectors, for $24$ element writes during merging. This is not a count of comparisons, which depends on the data. The clarity-first implementation also copies elements while constructing sub-vectors at every level, adding allocation and copying costs while preserving the same $\Theta(n \log n)$ time bound and $\Theta(n)$ peak auxiliary space. Quicksort follows the same divide-and-conquer skeleton with a different split. It partitions around a pivot instead of halving blindly and achieves $\Theta(n \log n)$ time on average, but can degrade to $\Theta(n^2)$ time and $\Theta(n)$ recursion depth with consistently poor pivots. The recurrence $T(n) = 2T(n/2) + \Theta(n)$ and its solution are instances of the Master Theorem, the general tool for solving divide-and-conquer recurrences, which the interested reader can pursue in Cormen and colleagues.

11. Common pitfalls

Recursion fails in a small number of characteristic ways, and every one of them is a violation of the three requirements from Section 3. The reader who keeps those requirements in mind will avoid nearly all recursion bugs, and recognise the rest quickly.

The first and most common failure is the missing or unreachable base case. A recursive function with no base case, or with a base case that the recursive calls never actually hit, recurses until the call stack from Section 4 is exhausted and the program dies with a stack overflow. If a signed argument repeatedly follows the step n - 1 without a base-case guard, it passes zero and continues into negative values. With an unsigned argument such as the std::uint64_t used in our factorial, subtracting from zero wraps to the type’s maximum value. Neither case terminates correctly. The cure is to write the base case first, before the recursive step, and to check that every recursive call moves toward it.

The second failure is a recursive step that does not make progress. A call that reduces $n$ to $n$, or that recurses on the same collection it was given rather than a strictly smaller one, satisfies the letter of “it recurses” while violating the guarantee of termination; it too runs until the stack overflows. Progress must be toward the base case and it must be strict, which is the same strictly smaller we insisted on in Section 2.

The third failure is not a crash but a performance disaster: redundant recomputation, the exponential blow-up of the naive Fibonacci in Section 5. The code is correct and it terminates; it is merely exponentially slower than it needs to be, because it solves the same subproblem many times. The cure is memoization or a bottom-up reformulation, as in Section 7. This pitfall is dangerous precisely because it is invisible on small inputs. fibonacci(10) returns instantly, and the problem reveals itself only when the input grows and the running time explodes. The final caution is quieter still: even a correct, non-redundant recursion that descends $\Theta(n)$ deep can overflow the stack for large enough $n$ on platforms without tail-call optimisation. This is why production quicksort implementations must prevent adversarially unbalanced recursion, and why deep linear recursion is often rewritten with an explicit stack or as an iterative loop.

12. Conclusion

We began with an act of faith: assume the smaller problem is already solved. We then spent the rest of the article showing that this faith is well founded only when it is disciplined. Mathematical induction supplies the discipline and the correctness proof; the base case and strict progress supply termination; the call stack supplies the mechanism and the space cost; the recursion tree exposes the time cost, exponential and redundant in the naive Fibonacci; and memoization, tail recursion, and divide-and-conquer supply the repairs and the real power. Mutual recursion and the pitfalls round out the picture.

If a single idea deserves to be carried forward, it is this: recursion is the clearest language in which to state a self-similar problem, but stating a solution and running it efficiently are different acts. The gap between them, measured by the recursion tree and closed by dynamic programming, is where most of the interesting engineering lives. The reader who internalises that gap will reach for recursion to think, and for memoization or iteration to ship, and will rarely be surprised by either.

References

ABELSON, H.; SUSSMAN, G. J.; SUSSMAN, J. Structure and Interpretation of Computer Programs. 2nd ed. Cambridge: MIT Press, 1996.

CHURCH, A. An Unsolvable Problem of Elementary Number Theory. American Journal of Mathematics, v. 58, n. 2, p. 345–363, 1936. DOI: https://doi.org/10.2307/2371045.

CORMEN, T. H.; LEISERSON, C. E.; RIVEST, R. L.; STEIN, C. Introduction to Algorithms. 4th ed. Cambridge: MIT Press, 2022.

GÖDEL, K. Über formal unentscheidbare Sätze der Principia Mathematica und verwandter Systeme I. Monatshefte für Mathematik und Physik, v. 38, p. 173–198, 1931. DOI: https://doi.org/10.1007/BF01700692.

GRAHAM, R. L.; KNUTH, D. E.; PATASHNIK, O. Concrete Mathematics: A Foundation for Computer Science. 2nd ed. Reading: Addison-Wesley, 1994.

MCCARTHY, J. Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I. Communications of the ACM, v. 3, n. 4, p. 184–195, 1960. DOI: https://doi.org/10.1145/367177.367199.

TURING, A. M. On Computable Numbers, with an Application to the Entscheidungsproblem. Proceedings of the London Mathematical Society, s2-42, p. 230–265, 1937. DOI: https://doi.org/10.1112/plms/s2-42.1.230.

(Updated: )