Python · Syntax · Intermediate

Recursion

10 tasks

Functions that call themselves. Covers base cases, recursive patterns, and when recursion is the right tool.

How recursion works: base case, recursive case, call stack

#
A recursive function calls itself to solve a smaller version of the same problem. Every recursive function needs two things: a **base case** that stops the recursion, and a **recursive case** that moves toward the base case. ```python def factorial(n): if n == 0: # base case: stop here return 1 return n * factorial(n - 1) # recursive case: smaller problem factorial(4) # 24 ``` **What happens on each call — the call stack** When Python calls a function, it pushes a frame onto the call stack. Each frame holds local variables and a return address. With `factorial(4)`, the stack grows like this: ``` factorial(4) -> calls factorial(3) waits... factorial(3) -> calls factorial(2) waits... factorial(2) -> calls factorial(1) waits... factorial(1) -> calls factorial(0) waits... factorial(0) -> returns 1 (base case hit!) factorial(1) returns 1 * 1 = 1 factorial(2) returns 2 * 1 = 2 factorial(3) returns 3 * 2 = 6 factorial(4) returns 4 * 6 = 24 ``` The stack *unwinds* after the base case is reached — each waiting call gets its answer and computes its own return value. **The most common mistake: forgetting the base case** Without a base case, recursion never stops — Python raises `RecursionError`: ```python def bad_factorial(n): return n * bad_factorial(n - 1) # no base case! bad_factorial(3) # RecursionError: maximum recursion depth exceeded ``` **Another common mistake: not moving toward the base case** ```python def also_bad(n): if n == 0: return 0 return also_bad(n + 1) # goes away from 0, not toward it! ``` Always verify: does each recursive call make the problem *strictly smaller*? **Simple example: sum of a list** ```python def total(lst): if not lst: # base case: empty list return 0 return lst[0] + total(lst[1:]) # first element + sum of rest total([1, 2, 3, 4]) # 10 ``` This is less efficient than `sum(lst)` but shows the recursive pattern clearly: split off one element, recurse on the rest, combine.

When recursion is the right tool: trees, divide and conquer

#
Recursion shines when the problem has a naturally recursive structure: trees, nested data, divide-and-conquer algorithms. **Binary search** (divide and conquer) ```python def binary_search(lst, target, lo=0, hi=None): if hi is None: hi = len(lst) - 1 if lo > hi: # base case: not found return -1 mid = (lo + hi) // 2 if lst[mid] == target: return mid if lst[mid] < target: return binary_search(lst, target, mid + 1, hi) # search right half return binary_search(lst, target, lo, mid - 1) # search left half binary_search([1, 3, 5, 7, 9], 7) # 3 ``` **Tree traversal** (naturally recursive structure) ```python def tree_sum(node): if node is None: # base case: empty node return 0 return node['value'] + tree_sum(node.get('left')) + tree_sum(node.get('right')) tree = {'value': 1, 'left': {'value': 2, 'left': None, 'right': None}, 'right': {'value': 3, 'left': None, 'right': None}} tree_sum(tree) # 6 ``` **Flatten nested lists** ```python def flatten(lst): result = [] for item in lst: if isinstance(item, list): result.extend(flatten(item)) # recurse into nested list else: result.append(item) return result flatten([1, [2, [3, 4]], 5]) # [1, 2, 3, 4, 5] ``` **Tail recursion — Python doesn't optimize it** In some languages (Scheme, Erlang), a recursive call that is the *last* operation in the function (tail call) is optimized by the compiler to reuse the stack frame. Python deliberately does NOT do this (Guido van Rossum: it would make tracebacks unreadable). This means a tail-recursive Python function still creates O(n) stack frames: ```python def factorial_tail(n, acc=1): if n == 0: return acc return factorial_tail(n - 1, acc * n) # tail call - but NOT optimized in Python ``` If you need to recurse very deeply (thousands of calls), convert to iteration instead.

Recursion vs iteration, lru_cache, and converting to a stack

#
**Recursion vs iteration — when to switch** Recursion is elegant for naturally hierarchical problems. Iteration is often better when: the recursion depth could be large, or the structure is linear (linked list, flat sequence). ```python # Recursive - clear but O(n) stack frames def sum_recursive(n): if n == 0: return 0 return n + sum_recursive(n - 1) # Iterative - same result, O(1) stack space def sum_iterative(n): total = 0 while n > 0: total += n n -= 1 return total ``` Python's default recursion limit is 1000. You can raise it with `sys.setrecursionlimit(n)`, but this is a band-aid — large recursion depth is usually a sign you should switch to iteration. **`functools.lru_cache` — memoize expensive recursive calls** Without caching, naive Fibonacci recalculates the same subproblems exponentially: ```python def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2) # O(2^n) calls fib(35) # takes ~2 seconds ``` Add `@lru_cache` and each subproblem is solved once: ```python from functools import lru_cache @lru_cache(maxsize=None) def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2) # O(n) calls now fib(35) # instant fib.cache_info() # CacheInfo(hits=..., misses=36, ...) ``` **Converting recursion to iteration with an explicit stack** Any recursion can be converted to iteration by maintaining your own stack (a list). This avoids Python's recursion limit entirely: ```python def flatten_iterative(lst): result = [] stack = [lst] while stack: current = stack.pop() for item in reversed(current): if isinstance(item, list): stack.append(item) # push nested list for later else: result.append(item) return result flatten_iterative([1, [2, [3, 4]], 5]) # [1, 2, 3, 4, 5] ``` **Rule of thumb**: use recursion when the depth is bounded and small (e.g., tree height < 100), and `lru_cache` when subproblems overlap. Switch to iteration (with an explicit stack) when depth is unbounded or performance is critical.

Classic recursion patterns cookbook

#
Classic recursion patterns worth knowing. **Power (exponentiation by squaring)** ```python def power(base, exp): if exp == 0: return 1 if exp % 2 == 0: half = power(base, exp // 2) return half * half # O(log n) instead of O(n) return base * power(base, exp - 1) power(2, 10) # 1024 ``` **Permutations of a list** ```python def permutations(lst): if len(lst) <= 1: return [lst] result = [] for i, item in enumerate(lst): rest = lst[:i] + lst[i+1:] for perm in permutations(rest): result.append([item] + perm) return result permutations([1, 2, 3]) # [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] ``` **Power set (all subsets)** ```python def power_set(lst): if not lst: return [[]] first, rest = lst[0], lst[1:] subsets = power_set(rest) return subsets + [[first] + s for s in subsets] power_set([1, 2, 3]) # [[], [3], [2], [2,3], [1], [1,3], [1,2], [1,2,3]] ``` **Merge sort** ```python def merge_sort(lst): if len(lst) <= 1: return lst mid = len(lst) // 2 left = merge_sort(lst[:mid]) right = merge_sort(lst[mid:]) return merge(left, right) def merge(a, b): result = [] i = j = 0 while i < len(a) and j < len(b): if a[i] <= b[j]: result.append(a[i]); i += 1 else: result.append(b[j]); j += 1 return result + a[i:] + b[j:] merge_sort([5, 2, 8, 1, 9]) # [1, 2, 5, 8, 9] ``` **Pattern summary** ``` Problem type Recursion structure ──────────────────── ────────────────────────────────────────── Linear (list, string) split off one element, recurse on rest Divide & conquer split in half, recurse both, merge Tree/graph recurse on children/neighbors Combinatorics choose one item, recurse on remainder Overlapping subproblems add @lru_cache to any of the above ```
01

Factorial

#

Write a recursive function that computes the factorial of a non-negative integer n (n! = n × (n−1) × ... × 1, with 0! = 1).

def factorial(n):
    pass


print(factorial(0))
print(factorial(5))
print(factorial(10))
Solution
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)


print(factorial(0))
print(factorial(5))
print(factorial(10))
02

Sum of a list

#

Write a recursive function that computes the sum of all numbers in a list, without using built-in sum() or loops.

def recursive_sum(numbers):
    pass


print(recursive_sum([1, 2, 3, 4, 5]))
print(recursive_sum([]))
Solution
def recursive_sum(numbers):
    if not numbers:
        return 0
    return numbers[0] + recursive_sum(numbers[1:])


print(recursive_sum([1, 2, 3, 4, 5]))
print(recursive_sum([]))
03

Power

#

Write a recursive function that computes base raised to the power of exponent. Do not use the ** operator or math.pow().

def power(base, exponent):
    pass


print(power(2, 10))
print(power(3, 4))
print(power(5, 0))
Solution
def power(base, exponent):
    if exponent == 0:
        return 1
    return base * power(base, exponent - 1)


print(power(2, 10))
print(power(3, 4))
print(power(5, 0))
04

Fibonacci

#

Write a recursive function that returns the nth Fibonacci number. The sequence starts: 0, 1, 1, 2, 3, 5, 8, 13... (fib(0)=0, fib(1)=1, fib(n)=fib(n-1)+fib(n-2)).

def fib(n):
    pass


for i in range(8):
    print(fib(i), end=" ")
Solution
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)


for i in range(8):
    print(fib(i), end=" ")
05

Reverse a string

#

Write a recursive function that reverses a string.

def reverse_string(s):
    pass


print(reverse_string("hello"))
print(reverse_string(""))
print(reverse_string("a"))
Solution
def reverse_string(s):
    if len(s) <= 1:
        return s
    return reverse_string(s[1:]) + s[0]


print(reverse_string("hello"))
print(reverse_string(""))
print(reverse_string("a"))
06

Count occurrences

#

Write a recursive function that counts how many times a target value appears in a nested list (a list that may contain other lists).

def count_in_nested(data, target):
    pass


data = [1, [2, 1, [3, 1]], [1, 4]]
print(count_in_nested(data, 1))
Solution
def count_in_nested(data, target):
    count = 0
    for item in data:
        if isinstance(item, list):
            count += count_in_nested(item, target)
        elif item == target:
            count += 1
    return count


data = [1, [2, 1, [3, 1]], [1, 4]]
print(count_in_nested(data, 1))
07

Flatten nested list

#

Write a recursive function that takes a nested list of any depth and returns a single flat list of all values.

def flatten(data):
    pass


print(flatten([1, [2, [3, [4]], 5], 6]))
Solution
def flatten(data):
    result = []
    for item in data:
        if isinstance(item, list):
            result.extend(flatten(item))
        else:
            result.append(item)
    return result


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

Binary search (recursive)

#

Write a recursive function that performs binary search on a sorted list. Return the index of the target if found, or -1 if not.

def binary_search(arr, target, low=0, high=None):
    pass


nums = [1, 3, 5, 7, 9, 11, 13, 15]
print(binary_search(nums, 7))
print(binary_search(nums, 6))
Solution
def binary_search(arr, target, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low > high:
        return -1
    mid = (low + high) // 2
    if arr[mid] == target:
        return mid
    if arr[mid] < target:
        return binary_search(arr, target, mid + 1, high)
    return binary_search(arr, target, low, mid - 1)


nums = [1, 3, 5, 7, 9, 11, 13, 15]
print(binary_search(nums, 7))
print(binary_search(nums, 6))
09

Count digits

#

Write a recursive function that counts the number of digits in a positive integer, without converting it to a string.

def count_digits(n):
    pass


print(count_digits(1))
print(count_digits(42))
print(count_digits(12345))
Solution
def count_digits(n):
    if n < 10:
        return 1
    return 1 + count_digits(n // 10)


print(count_digits(1))
print(count_digits(42))
print(count_digits(12345))
10

Tower of Hanoi

#

Write a recursive function that prints the steps to solve the Tower of Hanoi puzzle for n disks. Move all disks from peg A to peg C using peg B as auxiliary. Print each move as 'Move disk from X to Y'.

def hanoi(n, source, target, auxiliary):
    pass


hanoi(3, 'A', 'C', 'B')
Solution
def hanoi(n, source, target, auxiliary):
    if n == 1:
        print(f"Move disk from {source} to {target}")
        return
    hanoi(n - 1, source, auxiliary, target)
    print(f"Move disk from {source} to {target}")
    hanoi(n - 1, auxiliary, target, source)


hanoi(3, 'A', 'C', 'B')