Common Algorithms
Recursion Introduction
Many programming problems involve repeating the same operation on smaller pieces of data, like calculating compound interest or navigating folder structures. Recursion provides an elegant way to solve these problems by having a function call itself with progressively simpler inputs until reaching a trivial case.
Characteristics
- Elegant: solves complex problems with simple code
- Memory: uses call stack
- Performance: often slower than iteration
- Natural fit: trees, graphs, divide-and-conquer
When to Use
- Problem naturally recursive
- Divide and conquer algorithms
- Backtracking problems
- Mathematical sequences
When to Avoid
- Simple loops work better
- Very deep recursion
- Performance critical code
- Python recursion limit concerns
Fibonacci Example
base_case.py
# Importance of base case
import sys
def infinite_recursion(n):
"""Missing base case (don't run!)"""
# No base case - will cause RecursionError
return infinite_recursion(n - 1)
def countdown(n):
"""Correct with base case"""
# Base case
if n <= 0:
print("Liftoff!")
return 0
# Recursive case
print(n)
return countdown(n - 1)
def bad_base_case(n):
"""Wrong base case"""
# Base case never reached for negative input
if n == 0:
return 0
return bad_base_case(n - 1)
# Test base cases
print("Countdown with proper base case:")
countdown(5)
print("\nTrying infinite recursion with small depth:")
# Set a smaller recursion limit for demonstration
old_limit = sys.getrecursionlimit()
sys.setrecursionlimit(20)
try:
# This will overflow quickly
infinite_recursion(10)
except RecursionError:
print("RecursionError caught!")
# Restore original limit
sys.setrecursionlimit(old_limit)
print("\nWrong base case with positive input (safe):")
bad_base_case(3)
print("Completed successfully")
# Uncommenting this will cause RecursionError
# print("\nWrong base case with negative input:")
# bad_base_case(-5)
factorial.py
# Basic factorial recursion
def factorial(n):
"""Recursive factorial"""
# Base case
if n <= 1:
return 1
# Recursive case
# n! = n × (n-1)!
return n * factorial(n - 1)
# Test factorial
limit =
for i in range(limit):
print(f"{i}! = {factorial(i)}")
print(f"\n10! = {factorial(10)}")
# Basic factorial recursion
def factorial(n):
"""Recursive factorial"""
# Base case
if n <= 1:
return 1
# Recursive case
# n! = n × (n-1)!
return n * factorial(n - 1)
# Test factorial
limit =
for i in range(limit):
print(f"{i}! = {factorial(i)}")
print(f"\n10! = {factorial(10)}")
# Basic factorial recursion
def factorial(n):
"""Recursive factorial"""
# Base case
if n <= 1:
return 1
# Recursive case
# n! = n × (n-1)!
return n * factorial(n - 1)
# Test factorial
limit =
for i in range(limit):
print(f"{i}! = {factorial(i)}")
print(f"\n10! = {factorial(10)}")
trace.py
# Factorial with trace
depth = 0
def factorial(n):
"""Factorial with trace"""
global depth
# Indent based on depth
indent = " " * depth
print(f"{indent}→ factorial({n})")
depth += 1
# Base and recursive cases
if n <= 1:
result = 1
print(f"{indent} Base case: return 1")
else:
result = n * factorial(n - 1)
print(f"{indent} Return {n} * factorial({n-1}) = {result}")
depth -= 1
print(f"{indent}← returning {result}")
return result
# Test with trace
print("Computing factorial(5):\n")
result = factorial(5)
print(f"\nFinal result: {result}")
vs_iteration.py
# Recursion vs iteration
import time
def sum_recursive(n):
"""Recursive sum"""
# Base case
if n <= 0:
return 0
# Recursive case
return n + sum_recursive(n - 1)
def sum_iterative(n):
"""Iterative sum"""
# Loop approach
total = 0
for i in range(1, n + 1):
total += i
return total
def power_recursive(base, exp):
"""Recursive power"""
# Base case
if exp == 0:
return 1
# Recursive case
return base * power_recursive(base, exp - 1)
def power_iterative(base, exp):
"""Iterative power"""
# Loop approach
result = 1
for _ in range(exp):
result *= base
return result
# Compare both approaches
print("Sum 1 to 10:")
print(f" Recursive: {sum_recursive(10)}")
print(f" Iterative: {sum_iterative(10)}")
print("\n2^8:")
print(f" Recursive: {power_recursive(2, 8)}")
print(f" Iterative: {power_iterative(2, 8)}")
# Performance comparison kept small so trace replay stays readable.
n = 12
start = time.perf_counter()
r1 = sum_recursive(n)
time_recursive = time.perf_counter() - start
start = time.perf_counter()
r2 = sum_iterative(n)
time_iterative = time.perf_counter() - start
print(f"\nPerformance (sum to {n}):")
print(f" Recursive: {time_recursive * 1000:.4f} ms")
print(f" Iterative: {time_iterative * 1000:.4f} ms")
print(f" Speedup: {time_recursive / time_iterative:.2f}x")
fibonacci.py
# Fibonacci sequence
def fibonacci(n):
"""Recursive fibonacci"""
# Base cases
if n <= 1:
return n
# Recursive case
# fib(n) = fib(n-1) + fib(n-2)
return fibonacci(n - 1) + fibonacci(n - 2)
# Fibonacci with call counting
call_count = 0
def fibonacci_counted(n):
"""Fibonacci with call counting"""
global call_count
call_count += 1
if n <= 1:
return n
return fibonacci_counted(n - 1) + fibonacci_counted(n - 2)
# Test fibonacci
print("Fibonacci sequence:")
for i in range(7):
print(f"fib({i}) = {fibonacci(i)}")
# Count calls for fib(6)
call_count = 0
result = fibonacci_counted(6)
print(f"\nfib(6) = {result}, required {call_count} calls")
# Count calls for fib(8)
call_count = 0
result = fibonacci_counted(8)
print(f"fib(8) = {result}, required {call_count} calls")
base_case
The condition where recursion stops and returns a direct value without further recursive calls
factorial
A mathematical function where n! = n * (n-1) * ... * 1, naturally expressed recursively
Exercise: practical.py
Implement a recursive countdown function and a recursive sum of digits function