Python Interactive Guide - Step 3 Functions (6) - Recursive Functions
- This course, Python Interactive Guide, is designed to help you learn the basics of Python programming through hands-on, interactive examples.
- The “Style Guide” sections introduce clean coding practices, mainly based on PEP8.
- You can run and experiment with every code example.
Feel free to try things out - reloading the page will reset everything.
This is a continuation of “Step 3 Functions.”
3.5. Recursive Functions
Let’s start by looking at the following code:
In this program, the function calls itself (factorial
) from within its definition.
This mechanism where a function calls itself is called recursion, and functions that use recursion are called recursive functions.
3.5.1. Basics of Recursive Functions
To understand how recursive functions work, it’s helpful to trace the factorial calculation step by step.
When we call factorial(5)
, the following steps happen in sequence:
- Execute
factorial(5)
- Need to return
5 * factorial(4)
, but first need the result offactorial(4)
- Need to return
- Execute
factorial(4)
- Need to return
4 * factorial(3)
, but first need the result offactorial(3)
- Need to return
- Execute
factorial(3)
- Need to return
3 * factorial(2)
, but first need the result offactorial(2)
- Need to return
- Execute
factorial(2)
- Need to return
2 * factorial(1)
, but first need the result offactorial(1)
- Need to return
- Execute
factorial(1)
- Since
n <= 1
, return1
- Since
- Resume
factorial(2)
processing, return2 * 1 = 2
- Resume
factorial(3)
processing, return3 * 2 = 6
- Resume
factorial(4)
processing, return4 * 6 = 24
- Resume
factorial(5)
processing, return5 * 24 = 120
as the final result
The following code lets us see what’s happening at each step:
When creating a recursive function, two key elements are needed:
- Base Case: The condition where recursion ends
- In the example above, when
n <= 1
, it returns1
without further recursion
- In the example above, when
- Recursive Step: Breaking the problem into smaller parts and calling itself
- In the example above,
n * factorial(n - 1)
breaks it into smaller problems
- In the example above,
3.5.2. Examples of Recursive Functions
Recursive functions are particularly effective when a problem can be broken down into smaller problems of the same form.
Let’s look at some classic examples.
Fibonacci Sequence
The Fibonacci sequence is a series where each number is the sum of the two preceding ones:
0
th number:0
1
st number:1
n
th number:n-1
th number +n-2
th number (forn
>= 2)
This sequence can be naturally expressed as a recursive function:
Recursive Search in Hierarchical Structures
Recursive functions are also effective when dealing with hierarchical structures.
Here’s an example that visually outputs an organizational structure in a tree format:
Create a recursive function flatten
to flatten nested lists
(e.g., flatten([1, [2, [3, 4], 5]) = [1, 2, 3, 4, 5]
)
Example Solution
Create a recursive function collatz
that returns the number of steps needed for a positive integer n
to reach 1 following these rules:
- If
n
is 1: end the process - If
n
is even: dividen
by 2 - If
n
is odd: multiplyn
by 3 and add 1
(For example: with n=6
, the sequence is 6 → 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1, so collatz(6) = 8
)
Example Solution
3.5.3. Memoization
The fibonacci
function we used earlier becomes extremely inefficient as n
increases exponentially:
To solve this problem, we can use a technique called memoization. Memoization stores the results of expensive function calls and reuses them when the same inputs occur again.
For example, when calculating fibonacci(5)
:
- Calculate
fibonacci(4)
+fibonacci(3)
- To calculate
fibonacci(4)
, we needfibonacci(3)
+fibonacci(2)
- To calculate
fibonacci(3)
, we needfibonacci(2)
+fibonacci(1)
As you can see, calculations for the same arguments (like fibonacci(3)
) happen multiple times.
With memoization, we can avoid these redundant calculations and get results more efficiently.
Python has a feature called decorators that can modify functions. The cache
decorator from the functools
module makes it easy to memoize functions (decorators will be explained in more detail later):
- Memoization is effective for functions that always return the same output for the same input.
For functions whose output depends on factors other than their input (e.g., a function returning the current time), memoization may cause unintended behavior. - Memoization consumes memory, so you need to consider the trade-off between memory usage and processing speed.
- When using the
cache
decorator fromfunctools
, function arguments must be immutable (hashable).
There’s a significant difference in computational complexity between simple recursive implementation and memoized implementation of the Fibonacci sequence.
Computational Complexity of Simple Recursion
Simple recursive implementation of the Fibonacci sequence has a time complexity of O(2^n).
This is because each call generates two recursive calls, and the same values are recalculated multiple times.
For example, when calculating fibonacci(5)
:
fibonacci(5)
calculatesfibonacci(4)
andfibonacci(3)
fibonacci(4)
calculatesfibonacci(3)
andfibonacci(2)
fibonacci(3)
calculatesfibonacci(2)
andfibonacci(1)
As you can see, values like fibonacci(3)
and fibonacci(2)
are calculated multiple times, causing the number of calculations to grow exponentially.
graph TD F5["fibonacci(5)"] F54["fibonacci(4)"] F543["fibonacci(3)"] F5432["fibonacci(2)"] F5431["fibonacci(1)"] F54321["fibonacci(1)"] F54320["fibonacci(0)"] F542["fibonacci(2)"] F5421["fibonacci(1)"] F5420["fibonacci(0)"] F53["fibonacci(3)"] F532["fibonacci(2)"] F5321["fibonacci(1)"] F5320["fibonacci(0)"] F531["fibonacci(1)"] F5 --> F54 F5 --> F53 F54 --> F543 F54 --> F542 F543 --> F5432 F543 --> F5431 F5432 --> F54321 F5432 --> F54320 F542 --> F5421 F542 --> F5420 F53 --> F532 F53 --> F531 F532 --> F5321 F532 --> F5320
Improvement with Memoization
With memoization, the time complexity improves dramatically to O(n).
For example, when calculating fibonacci(5)
, we need values from fibonacci(4)
to fibonacci(0)
multiple times, but with memoization, each value only needs to be calculated once, effectively requiring just 5 calculations.
graph TD F5["fibonacci(5)"] F54["fibonacci(4)"] F543["fibonacci(3)"] F5432["fibonacci(2)"] F5431["fibonacci(1)"] F54321["fibonacci(1)"] F54320["fibonacci(0)"] F542["fibonacci(2)"] F5421["fibonacci(1)"] F5420["fibonacci(0)"] F53["fibonacci(3)"] F532["fibonacci(2)"] F5321["fibonacci(1)"] F5320["fibonacci(0)"] F531["fibonacci(1)"] F5 --> F54 F5 --> F53 F54 -. Retrieved from memo .-> F543 F54 -. Retrieved from memo .-> F542 F543 -.-> F5432 F543 -.-> F5431 F5432 -.-> F54321 F5432 -.-> F54320 F542 -.-> F5421 F542 -.-> F5420 F53 --> F532 F53 --> F531 F532 -. Retrieved from memo .-> F5321 F532 --> F5320 %% Memoized nodes have gray border, gray background, semi-transparent style F543 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3 style F542 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3 style F5321 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3 style F5432 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3 style F5431 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3 style F54321 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3 style F54320 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3 style F5421 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3 style F5420 fill:#cccccc,stroke:#666666,stroke-width:2px,opacity:0.3
Below is a benchmark comparison between simple recursive implementation and memoized implementation using a benchmarking library:
The benchmark results clearly show that computational complexity increases exponentially without memoization, while memoized implementation runs efficiently.
3.5.4. Recursion Depth
Every time a recursive function is called, information needed for execution (local variables, arguments, return address, etc.) is stacked in memory as a “stack frame”.
Since stack space is limited, there’s a limit to recursion depth.
When a function is called, the computer allocates a memory region called a “stack frame”.
This frame stores information needed to execute the function (local variables, arguments, return address, etc.).
In recursive functions, stack frames are handled as follows:
- Each time a function calls itself, a new stack frame is added to the stack
- When a function completes, its stack frame is removed (freed)
- In deep recursion, many frames exist simultaneously temporarily
graph TD A["factorial(5)
n=5"] --> B["factorial(4)
n=4"] B --> C["factorial(3)
n=3"] C --> D["factorial(2)
n=2"] D --> E["factorial(1)
n=1"]
The diagram above shows the stack frames formed during recursive calls to the factorial
function.
When the maximum recursion depth is reached, Python raises a RecursionError
:
In Python, the recursion depth limit varies by platform (OS and Python implementation), but it’s typically set around 1000. You can check it with this code:
This limit can be changed using sys.setrecursionlimit()
, but setting it too high risks running out of memory and causing a stack overflow error:
- Increasing the recursion depth limit is only a temporary solution
- For a fundamental solution, review your algorithm (replace with iterative processing or implement memoization)
- Excessively deep recursion not only risks stack overflow but also reduces processing efficiency
3.5.5. Recursion vs. Loops
Most processes that can be expressed with recursion can also be expressed with loops (iterative processing).
For example, factorial calculation can be done with a loop like this:
When deciding between recursion and loops, consider these points:
- Readability: For some problems, recursive expressions may be more natural and easier to understand
- Efficiency: Generally, loops use less memory and are more efficient
- Limitations: Recursion has depth limits, so it may reach limits with large inputs
The collatz
function we dealt with in the previous practice problem uses recursion, which has a maximum depth limitation.
Rewrite the collatz
function using a while
loop to make it work for any integer.
Example Solution
Summary
In this section, we learned the following key points about recursive functions:
- Concept of Recursive Functions: A mechanism where a function calls itself
- Base Case and Recursive Step: Two essential elements for correctly functioning recursive functions
- Examples of Recursion: Factorial, Fibonacci sequence, hierarchical structure traversal, etc.
- Memoization: An important technique to optimize recursive calculations
- Recursion Depth: There are depth limits to recursion based on the execution environment
- Recursion vs. Loops: Comparing their strengths, weaknesses, and when to use each
Recursion is a powerful technique that can express complex processes simply.
However, it’s important to understand its mechanisms and limitations to avoid pitfalls like infinite recursion and stack overflow.