Introduction to Recursion
Recursion is a technique in programming where a function calls itself in
order to solve a problem. It is based on the idea of breaking down a
large problem into smaller, more manageable subproblems. Each
recursive call solves a simpler version of the problem until a base case is
reached, at which point the recursion stops.
Example:
Consider the problem of calculating the factorial of a number.
Factorial of a number n is the product of all positive integers less than or
equal to n.
Example:
5! = 5 × 4 × 3 × 2 × 1 = 120
The factorial can be defined recursively as:
Base case: 0! = 1
Recursive case: n! = n × (n-1)!
How to implement factorial recursively in Python:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n - 1)
If n is 0, the function returns 1.
Otherwise, the function calls itself with the value n-1 and multiplies n with
the result.
Visualization:
Example, if we call factorial(5), the function calls itself as:
factorial(5) → 5 * factorial(4)
factorial(4) → 4 * factorial(3)
factorial(3) → 3 * factorial(2)
factorial(2) → 2 * factorial(1)
factorial(1) → 1 * factorial(0)
factorial(0) → returns 1 (base case)
Recursion starts returning and multiplying the numbers together.
Problem Solving with Recursion
Recursion is particularly useful when a problem can naturally be divided
into smaller subproblems of the same type. Some problems that are
traditionally solved using recursion include:
Tree traversals -----> For example, traversing the nodes of a binary
tree.
Sorting algorithms -----> Like quicksort and merge sort.
Searching algorithms -----> Like binary search.
Combinatorial problems -----> Like generating permutations and
combinations.
Sum of an Array using Recursion
Calculate the sum of an array recursively by breaking the problem into
smaller subarrays.
def sum_array(arr):
if len(arr) == 0: # Base case: if the array is empty
return 0
else: # Recursive case
return arr[0] + sum_array(arr[1:])
If the array is empty, the sum is 0 (base case).
Otherwise, we add the first element of the array to the sum of the rest of
the array (recursive case).
For an array like [1, 2, 3], the recursive calls would look like this:
sum_array([1, 2, 3]) → 1 + sum_array([2, 3])
sum_array([2, 3]) → 2 + sum_array([3])
sum_array([3]) → 3 + sum_array([])
sum_array([]) → returns 0 (base case)
Thus, sum_array([1, 2, 3]) = 1 + 2 + 3 = 6.
Recursive Helper Functions
In some cases, recursion might require helper functions. These are
additional functions that handle specific parts of the recursion or provide
auxiliary tasks that assist the primary recursive function.
Helper functions are typically used to manage internal states (like
maintaining the current index or adjusting parameters). They also allow us
to design more flexible recursive solutions.
Tail Recursion Using Helper Functions
A helper function for a tail recursive factorial example.
def factorial(n):
return factorial_helper(n, 1)
def factorial_helper(n, accumulator):
if n == 0:
return accumulator
else:
return factorial_helper(n-1, accumulator * n)
The factorial_helper function is the helper function. It takes an accumulator
that carries the product of the numbers. This avoids the need for additional
stack frames and allows us to optimize recursion for tail calls (more on this
later).
Recursion vs. Iteration
Both recursion and iteration are methods to repeat a process until a
condition is met. However, they differ in terms of approach and
performance:
Recursion:
Each function call adds a new frame to the call stack, which may lead to a
stack overflow if the recursion depth is too high.
Recursion is often easier to conceptualize for problems like tree traversal,
searching, and combinatorics.
Recursion can be less efficient due to overhead from maintaining multiple
function calls in the call stack.
Iteration:
Uses a loop (like for or while) to repeat an operation. It doesn’t add new
frames to the call stack. Typically, more efficient than recursion because
there’s no overhead of function calls.
Iteration is usually better for problems that don’t have a natural recursive
structure.
Example: Comparing Recursion and Iteration for Factorial
Recursive factorial:
def factorial_recursive(n):
if n == 0:
return 1
return n * factorial_recursive(n - 1)
Iterative factorial:
def factorial_iterative(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
The iterative approach is more efficient because it doesn’t involve the
overhead of multiple function calls. Recursion is elegant, but for large n, an
iterative solution is generally preferred in terms of performance.
Tail Recursion
Tail recursion is a special kind of recursion where the recursive call is the
last operation in the function before returning a result. This is important
because some programming languages (like Lisp and Scheme) can optimize
tail recursive functions to avoid creating new stack frames, making them as
efficient as iteration.
In tail recursion, the function doesn’t need to keep track of its state between
calls since it immediately returns the result of the recursive call. The call
stack doesn’t grow, and the program executes with constant space
complexity.
Tail Recursion for Factorial
def factorial_tail_recursive(n):
return factorial_helper(n, 1)
def factorial_helper(n, accumulator):
if n == 0:
return accumulator
else:
return factorial_helper(n - 1, accumulator * n)
In this case, the recursive call is the last operation before returning, and the
accumulator carries the intermediate result, avoiding the need to wait for
the function to finish after deeper calls.