Previous | Tutorial index | Next

Tutorial 5: Recursive Functions – Functions That Call Themselves

Learning Objectives

Overview

Recursion is a programming technique where a function calls itself to solve a problem by breaking it down into smaller, similar sub‑problems. It is a powerful tool for problems that have a naturally recursive structure, such as traversing tree‑like data, processing nested structures, or implementing mathematical sequences like the Fibonacci numbers. However, recursion requires careful design to ensure it terminates—this is achieved with a base case. In this tutorial, we will explore the mechanics of recursion, the importance of the call stack, classic examples, performance considerations, and how to choose between recursion and iteration.

1. What Is Recursion?

A recursive function is a function that, during its execution, calls itself. This creates a chain of calls that gradually solves smaller instances of the same problem.

1.1 The Two Essential Elements

Every correct recursive function must have:

  1. Base Case – a condition that stops the recursion. When the base case is reached, the function returns a result without making further recursive calls.
  2. Recursive Case – the part where the function calls itself on a smaller or simpler version of the original problem. The recursive call must move closer to the base case.

Without a base case, the recursion would continue forever (or until Python raises a RecursionError due to depth limit).

2. How Recursion Works – The Call Stack

When a function calls itself, each call is placed on the call stack. The stack keeps track of each function call's state (local variables, return address). The calls are pushed onto the stack as they are made, and as the base case is reached, the stack unwinds (pops) as each function returns.

2.1 Visualizing Factorial

def factorial(n): if n == 1: return 1 return n * factorial(n - 1)

When you call factorial(4):

factorial(4) → n=4, calls factorial(3) factorial(3) → n=3, calls factorial(2) factorial(2) → n=2, calls factorial(1) factorial(1) → base case, returns 1 returns 2*1 = 2 returns 3*2 = 6 returns 4*6 = 24

Each call waits for the result of the next call, then multiplies and returns.

2.2 Stack Depth and Recursion Limit

Python limits the recursion depth to prevent stack overflow. The default limit is around 1000. You can check it with sys.getrecursionlimit() and adjust it with sys.setrecursionlimit(limit), but doing so is rarely recommended; it's safer to rewrite the algorithm iteratively or use a different approach.

3. Classic Recursive Examples

3.1 Factorial

Mathematically, n! = n * (n-1)! with 0! = 1 (or 1! = 1). Implementation:

def factorial(n): if n <= 1: return 1 return n * factorial(n - 1)

3.2 Fibonacci

The Fibonacci sequence: fib(0) = 0, fib(1) = 1, fib(n) = fib(n-1) + fib(n-2).

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

This naive recursive version is highly inefficient due to repeated computations (exponential time). We'll discuss optimizations later.

3.3 Summing a List

def sum_list(lst): if not lst: # empty list base case return 0 return lst[0] + sum_list(lst[1:])

3.4 Reversing a String

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

3.5 Power Function

def power(base, exp): if exp == 0: return 1 return base * power(base, exp - 1)

4. Recursion vs. Iteration

Aspect Recursion Iteration
Readability Often more concise and natural for recursive problems (e.g., tree traversal). Might be more straightforward for simple loops.
Performance Can be slower due to function call overhead; risk of stack overflow. Usually faster and memory‑efficient (no call stack growth).
Space Complexity O(depth) additional stack space. O(1) extra space (unless using data structures).
Ease of Implementation Elegant for divide‑and‑conquer, backtracking. Often easier for sequential operations.

When to use recursion?

When to avoid recursion?

5. Tail Recursion – A Special Case

A recursive function is tail recursive if the recursive call is the very last operation in the function (i.e., the function returns the result of the recursive call directly, without any additional computation). Some languages optimize tail recursion to avoid stack growth, but Python does not perform tail‑call optimization. However, it's still a good conceptual pattern.

Example of tail‑recursive factorial:

def factorial_tail(n, accumulator=1): if n <= 1: return accumulator return factorial_tail(n - 1, n * accumulator)

In Python, this still uses O(n) stack space.

6. Optimizing Recursive Fibonacci – Memoization

The naive Fibonacci has exponential time because it recalculates the same values many times. We can use memoization (caching results) to make it linear.

memo = {0: 0, 1: 1} def fib_memo(n): if n in memo: return memo[n] memo[n] = fib_memo(n - 1) + fib_memo(n - 2) return memo[n]

Or use functools.lru_cache:

from functools import lru_cache @lru_cache(maxsize=None) def fib_cached(n): if n <= 1: return n return fib_cached(n - 1) + fib_cached(n - 2)

7. Common Recursive Pitfalls

8. Advanced Recursive Patterns

8.1 Divide and Conquer

Split the problem into two or more sub‑problems, solve each recursively, and combine the results. Examples: merge sort, quick sort.

8.2 Backtracking

Explore all possible solutions by trying choices and undoing (backtracking) when a choice leads to a dead end. Classic examples: N‑Queens, Sudoku solver, generating permutations.

9. Practical Example – Directory Tree Traversal

Recursively list all files in a directory and its subdirectories:

import os def list_files(path): for entry in os.listdir(path): full = os.path.join(path, entry) if os.path.isdir(full): list_files(full) # recursive call else: print(full)

📝 Quiz – Check Your Understanding

  1. What is the base case in a recursive function?

    Answer(B) The condition that stops the recursion.
  2. What will happen if you forget to include a base case?

    Answer(B) It will run indefinitely or raise `RecursionError`.
  3. Given def f(n): if n == 0: return 1; return n * f(n-1), what does f(3) return?

    Answer(A) `6` – 3*2*1 = 6.
  4. What does the call stack do during recursion?

    Answer(B) It tracks the sequence of calls and their local variables.
  5. True or False: Python performs tail‑call optimization.

    AnswerFalse
  6. What is the main advantage of using recursion over iteration for tree traversal?

    Answer(B) It is often more concise and natural.
  7. How can you improve the performance of a naive Fibonacci recursive function?

    Answer(D) Both A and B.
  8. What is the default recursion limit in Python?

    Answer(B) 1000
  9. Which of the following is a tail‑recursive version of factorial?

    Answer(B) – tail recursive because the recursive call is the last operation.
  10. When should you avoid recursion?

    Answer(B) When the depth of recursion might be very large.

💻 Exercises – Practice Makes Perfect

Exercise 1: Sum of Digits
Write a recursive function digit_sum(n) that returns the sum of the digits of a non‑negative integer n.

Sample Solution ```python def digit_sum(n): if n < 10: return n return n % 10 + digit_sum(n // 10)

print(digit_sum(123)) # 6

</details> **Exercise 2: Power with Recursion** Write a recursive function `power(base, exponent)` that returns `base` raised to the `exponent`. <details><summary>Sample Solution</summary> ```python def power(base, exp): if exp == 0: return 1 return base * power(base, exp - 1) print(power(2, 3)) # 8

Exercise 3: Greatest Common Divisor
Write a recursive function gcd(a, b) using Euclid’s algorithm.

Sample Solution ```python def gcd(a, b): if b == 0: return a return gcd(b, a % b)

print(gcd(48, 18)) # 6

</details> **Exercise 4: Countdown** Write a recursive function `countdown(n)` that prints numbers from `n` down to `1`, then `"Blast off!"`. <details><summary>Sample Solution</summary> ```python def countdown(n): if n == 0: print("Blast off!") else: print(n) countdown(n-1) countdown(5)

Exercise 5: Palindrome Checker (Recursive)
Write a recursive function is_palindrome(s) that returns True if the string is a palindrome (ignoring spaces and case).

Sample Solution ```python def is_palindrome(s): s = ''.join(s.split()).lower() if len(s) <= 1: return True return s[0] == s[-1] and is_palindrome(s[1:-1])

print(is_palindrome("A man a plan a canal Panama")) # True

</details> --- ### 🏠 Homework – Deeper Thinking #### Short Answer Questions **1. Recursive Binary Search** Implement a recursive binary search function `binary_search(arr, target, low, high)` that returns the index or `-1`. <details><summary>Sample Answer</summary> ```python def binary_search(arr, target, low, high): if low > high: return -1 mid = (low + high) // 2 if arr[mid] == target: return mid elif arr[mid] < target: return binary_search(arr, target, mid+1, high) else: return binary_search(arr, target, low, mid-1)

2. Tower of Hanoi
Write a recursive function hanoi(n, source, target, auxiliary) that prints the moves.

Sample Answer ```python def hanoi(n, source, target, auxiliary): if n == 1: print(f"Move disk 1 from {source} to {target}") else: hanoi(n-1, source, auxiliary, target) print(f"Move disk {n} from {source} to {target}") hanoi(n-1, auxiliary, target, source) ```

3. Recursive Permutations
Write a function permutations(sequence) that returns a list of all permutations.

Sample Answer ```python def permutations(seq): if len(seq) <= 1: return[seq]result = [] fori,charinenumerate(seq):rest = seq[:i] +seq[i+1:]forperminpermutations(rest):result.append(char+perm)returnresult```

4. Merge Sort (Recursive)
Implement merge sort recursively with merge_sort(arr) and merge(left, right).

Sample Answer ```python def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]); i += 1 else: result.append(right[j]); j += 1 result.extend(left[i:]); result.extend(right[j:]) return result

def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) return merge(left, right)

</details> #### Essay Questions **5. Flatten Nested Lists** Write a recursive function `flatten(nested_list)` that returns a flat list. <details><summary>Sample Answer</summary> ```python def flatten(nested): result = [] for item in nested: if isinstance(item, list): result.extend(flatten(item)) else: result.append(item) return result print(flatten([1, [2, [3, 4], 5], 6])) # [1,2,3,4,5,6]

Homework Hints

Summary

In this tutorial, you have learned:

Recursion is a powerful tool that can lead to elegant solutions for certain problems. However, it must be used judiciously, keeping performance and stack depth in mind. With practice, you will develop an intuition for when recursion is the right choice.

Next Steps: In Tutorial 6, we will dive into Higher‑Order Functions and Lambdas, exploring functions that operate on other functions, and the map, filter, and reduce functions.

Happy recursing!

Previous | Tutorial index | Next