5. Recursion#
In order to understand recursion, one must first understand recursion.
Recursion occurs when the definition of a concept or process depends on (a simpler or previous version of) itself.
In the context of modern computer programming, recursion refers to functions calling themselves.
These “recursive calls” are often made with smaller input data.
As you may have gathered from previous sections, a big theme in programming, and problem solving in general, is the idea of breaking down a problem into smaller, more manageable subproblems.
In case of decrease and conquer, we break down the problem into smaller subproblems in a manner that the subproblems are smaller than the original problem by a constant amount i.e. \(n_{new} = n_{original} - k\), where k is a constant. In case of divide and conquer, we break down the problem into smaller subproblems in a manner that the subproblems are smaller than the original problem by a constant factor i.e. \(n_{~new} = n_{~original} / k\), where k is a constant.
This process is repeated until the problem cannot be broken down any further, at which point the solutions to the subproblems are combined to solve the original problem.
In the context of lists, we reach this stage when the list is empty or has only one element.
5.1. ATM Example#
Let’s understand recursion with the help of an analogy.
Imagine you need some cash. You go to the ATM and see a long queue. The queue is so long that you can’t see the end of it.
You decide to find out how many people are there in the queue in a recursive manner. So you ask the person in front of you what number are they. The person in front of you asks the person in front of them and so on. In recursion, this is known as the recursive step.
This process continues until the person at the end of the queue is asked. This is known as the base case in recursion. The base case is the condition that stops the recursion. In this case, the base case is when the person at the end of the queue is asked.
The person at the end of the queue is asked and they reply that they are the first person in the queue.
The person in front of them adds 1 to the number and tells the person in front of them. This process continues until the person in front of you tells you the number.
def count(queue):
# BASE CASE: if there is only one person in the queue
if queue == ["person"]:
return 1
else:
# remove the first element
subset = queue[1:]
# RECURSIVE STEP: count the number of people ahead
count_ahead = count(subset)
# return 1 plus the number of people ahead
return 1 + count_ahead
data = ["person", "person", "person", "person", "person"]
count(data)
5
5.2. Key Terms#
In summary, recursion involves two key components:
Recursive Step: The step where the function calls itself, breaking down the input data into smaller subset(s).
The number of times the function calls itself is known as the depth of recursion.
Base Case: The case where the input data cannot be broken down any further. This is the condition that stops the recursion.
Without a base case, the function would continue to call itself indefinitely, leading to an infinite loop. This is known as infinite recursion.
Descending down, towards base case: The process of breaking down the input data into smaller subset(s) is sometimes referred to as descending down recursion chain/tree. Other terms used are unfolding or decomposing the problem.
Ascending up, away from base case: The process of combining the solutions of the subproblems to solve the original problem is sometimes referred to as ascending up recursion. Other terms used are combining or unwinding the problem.
5.3. Pros and Cons of Recursion#
Recursion is a powerful tool, but it’s not always the best tool for the job. Here are some pros and cons of using recursion.
✅ Readable and Elegant: Recursive solutions are often more readable and elegant than their iterative counterparts.
✅ Mathematical: Many mathematical ideas are naturally recursive, so recursion can be a more organic way to express these ideas in code. For example, the Fibonacci sequence is defined recursively i.e. \(F(n) = F(n-1) + F(n-2)\). Similarly, the factorial of a number is defined recursively i.e. \(n! = n \times (n-1)!\).
✅ Divide and Conquer and Tree Structures: Recursive solutions are often well-suited for problems involving trees and graphs, which are ubiqutous in nature, sciences and particularly in computational problems. Since divide and conquer strategies yield a tree like structure, recursion is a natural fit for such problems.
❌ Performance: Recursive solutions are often less efficient than iterative solutions due to the overhead of function calls and the potential for repeated work.
❌ Debugging: Recursive solutions are often harder to debug than iterative solutions. This is because the state of the program is spread across many function calls, making it harder to reason about the program’s behavior.
❌ Memory: Recursive solutions often require more memory than iterative solutions. This is because each function call requires memory to store the function’s state, and the depth of recursion can be quite large.
❌ Convoluted: Recursive solutions can be harder to understand than iterative solutions, especially for people who are new to programming.