Learn / DS & Algo / Algorithms / Recursion

Intermediate 15 min

Recursion

Base cases, recursive cases, the call stack and when to avoid recursion.

What you will learn

  • Identify base and recursive cases
  • Trace the call stack
  • Convert recursion to iteration

A recursive function solves a problem by calling itself on a smaller version of the same problem. It feels odd at first, but many problems (trees, nested folders, divide-and-conquer) are naturally recursive, and the code ends up shorter than a loop.

Two required parts

  • Base case: the smallest problem, answered directly. Without it, the function never stops.
  • Recursive case: reduce the problem and call yourself, trusting that the call returns the right answer for the smaller input.
def factorial(n):
    if n <= 1:               # base case
        return 1
    return n * factorial(n - 1)   # recursive case

print(factorial(5))
Output
120

The call stack

Each call waits for the one it started. factorial(3) becomes 3 * factorial(2), which becomes 3 * (2 * factorial(1)); when the base case returns 1, the stack unwinds multiplying as it goes. Each waiting call uses memory, so Python limits depth (about 1000) and raises RecursionError if you exceed it.

def sum_list(a):
    return 0 if not a else a[0] + sum_list(a[1:])

def reverse(s):
    return s if len(s) <= 1 else reverse(s[1:]) + s[0]

def power(x, n):                    # fast exponent, O(log n)
    if n == 0:
        return 1
    half = power(x, n // 2)
    return half * half * (x if n % 2 else 1)

print(sum_list([1, 2, 3]), reverse("abc"), power(2, 10))
Output
6 cba 1024

Recursion on nested data

Recursion shines when data has no fixed depth, like a nested list or a directory tree.

def flatten(x):
    out = []
    for item in x:
        if isinstance(item, list):
            out.extend(flatten(item))
        else:
            out.append(item)
    return out

print(flatten([1, [2, [3, 4]], 5]))
Output
[1, 2, 3, 4, 5]

The Fibonacci trap

Naive fib(n) = fib(n-1) + fib(n-2) recomputes the same values over and over and takes exponential time. Caching results fixes it, which leads straight into dynamic programming.

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(50))
Output
12586269025

Recursion vs iteration

Anything recursive can be written with a loop and an explicit stack. Prefer a loop when depth could be large or the loop is just as clear; prefer recursion when it mirrors the structure of the problem.

Debugging tip

Write the base case first, then check that every recursive call moves strictly closer to it. Print the arguments at the top of the function to watch the stack build.

Try it yourself

Write a recursive is_palindrome(s) that returns True if a string reads the same backward.

Show solution
def is_palindrome(s):
    if len(s) <= 1:
        return True
    return s[0] == s[-1] and is_palindrome(s[1:-1])

print(is_palindrome("level"), is_palindrome("stack"))