DSA Recursion

Recursion is a programming technique where a function solves a problem by calling itself. Instead of solving the entire problem at once, a recursive function breaks the problem into smaller, simpler versions of the same problem and calls itself on each smaller version until it reaches a case simple enough to solve directly.

Recursion is one of the most important concepts in DSA. Many data structures (trees, graphs) and algorithms (merge sort, quicksort, DFS) are naturally expressed using recursion.

The Two Essential Parts of Recursion

Every recursive function must have two components. Without both, the function either gives wrong results or runs forever.

1. Base Case

The condition where the function stops calling itself. It is the simplest version of the problem that can be solved directly. Without a base case, recursion would continue infinitely, causing a stack overflow.

2. Recursive Case

The part where the function calls itself with a smaller or simpler input, moving step by step toward the base case.

A Simple First Example: Countdown


def countdown(n):
    if n == 0:           # Base case — stop here
        print("Done!")
        return
    print(n)
    countdown(n - 1)     # Recursive case — smaller input

countdown(5)
# Output:
# 5
# 4
# 3
# 2
# 1
# Done!

Each call to countdown prints one number and then calls itself with n - 1. When n reaches 0, the base case triggers and the recursion ends.

How Recursion Uses the Call Stack

Each time a function calls itself, a new stack frame is added to the call stack. Each frame holds the local variables for that call. When the base case is reached, the frames are resolved in reverse order (LIFO), and the results bubble back up.


# Call stack visualization for factorial(3)
# factorial(3) waits for factorial(2)
#   factorial(2) waits for factorial(1)
#     factorial(1) returns 1  <-- base case
#   factorial(2) returns 2 * 1 = 2
# factorial(3) returns 3 * 2 = 6

Classic Recursive Problems

1. Factorial

The factorial of a number n (written as n!) is the product of all positive integers from 1 to n. Example: 5! = 5 × 4 × 3 × 2 × 1 = 120.


def factorial(n):
    if n == 0 or n == 1:    # Base case
        return 1
    return n * factorial(n - 1)    # Recursive case

print(factorial(5))   # Output: 120
print(factorial(6))   # Output: 720

2. Fibonacci Sequence

Each Fibonacci number is the sum of the two before it: 0, 1, 1, 2, 3, 5, 8, 13 ...


def fibonacci(n):
    if n <= 1:              # Base case
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)   # Recursive case

print(fibonacci(7))   # Output: 13

Note: This naive version has O(2ⁿ) time complexity because it recalculates the same values many times. This will be optimized with memoization in the Dynamic Programming topic.

3. Sum of a List


def sum_list(lst):
    if len(lst) == 0:          # Base case — empty list
        return 0
    return lst[0] + sum_list(lst[1:])   # First element + sum of rest

print(sum_list([1, 2, 3, 4, 5]))   # Output: 15

4. Power of a Number


def power(base, exp):
    if exp == 0:               # Base case — anything to the power 0 is 1
        return 1
    return base * power(base, exp - 1)

print(power(2, 10))   # Output: 1024

5. Reversing a String


def reverse_string(s):
    if len(s) == 0:            # Base case
        return ""
    return reverse_string(s[1:]) + s[0]   # Recursive case

print(reverse_string("hello"))   # Output: olleh

Recursion vs Iteration

Most recursive problems can also be solved iteratively (using loops). The choice depends on clarity and context.

Factorial — Iterative Version


def factorial_iterative(n):
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

print(factorial_iterative(5))   # Output: 120

Both versions produce the same result. The recursive version is often more elegant for problems that are naturally recursive (like tree traversal). The iterative version avoids call stack overhead and is typically more memory-efficient.

Common Pitfalls in Recursion

Missing Base Case — Infinite Recursion


# WARNING: This will cause a RecursionError (stack overflow)
def broken(n):
    print(n)
    broken(n - 1)   # No base case — never stops!

Python's Recursion Limit

Python has a default recursion limit of 1000. For problems that require deeper recursion, the limit can be adjusted or an iterative approach should be used.


import sys
sys.setrecursionlimit(5000)   # Increase recursion limit (use with caution)

Tail Recursion

A recursive call is tail recursive if the recursive call is the very last operation in the function. Python does not optimize tail recursion, but understanding the concept is useful when working in other languages.


# Tail recursive factorial with accumulator
def factorial_tail(n, accumulator=1):
    if n == 0:
        return accumulator
    return factorial_tail(n - 1, n * accumulator)   # Tail position

print(factorial_tail(5))   # Output: 120

Key Points

  • Recursion is when a function calls itself to solve a smaller version of the same problem.
  • Every recursive function must have a base case (stop condition) and a recursive case (smaller input).
  • Recursion uses the call stack — deep recursion can cause stack overflow if the base case is never reached.
  • Classic recursive problems include factorial, Fibonacci, list sum, and string reversal.
  • Recursive solutions are often more readable for naturally hierarchical problems like trees.
  • Naive recursion can be slow (O(2ⁿ) for Fibonacci). Memoization and dynamic programming fix this — covered in later topics.

Leave a Comment