Implementing Recursion
Let's continue analyzing our PrintValues()
example:
To understand how this function works, it's helpful to trace the flow of execution step-by-step. Using the values a = 4
and b = 6
, the series of calls to the PrintValues()
method would look like this:
Understanding Recursive Algorithms
Recursive algorithms generally have the following components:
-
Base Case: This is the simplest version of the problem, where the solution can be directly obtained without further recursion. The base case is crucial because it provides a stopping condition to prevent infinite recursion. In our example, the base case is:
Here, the function stops calling itself whena
is equal tob
. -
Recursive Call: This is where the function calls itself with a modified version of the original problem, bringing it closer to the base case. It is essential to ensure that each recursive call makes progress towards the base case. In our example:
Each call incrementsa
, moving closer to the base case. -
Processing (Work): This is the part of the function that performs any required computation before or after the recursive call. In our example:
The Role of the Stack
The stack, as discussed in the previous chapter, plays a crucial role in the execution of recursive methods. Each time the method is called, its current state—return address, parameters, and local variables—is pushed onto the stack1. This ensures that when the function returns, it can continue where it left off.
For instance, when PrintValues(5, 6)
is called, it is pushed onto the stack, followed by PrintValues(6, 6)
. Once the base case is reached, these calls are popped off the stack in reverse order, completing one after the other. This is known as the unwinding of the recursive calls. The stack operates as a Last In, First Out (LIFO) structure.
Another Example: The Enigma
Method
Consider the following method, which also demonstrates the components of a recursive function:
Exercise: Tracing the Enigma
Method
Work through the following calls to the Enigma()
method and predict what will be displayed on the screen:
Enigma(4)
Enigma(99)
Enigma(123)
Enigma(-3456)
Enigma(1234567)
Let's trace Enigma(99)
step-by-step:
99
is neither negative nor less than10
, so the finalelse
block is executed.- The method calls itself with
Enigma(99 / 10)
, which isEnigma(9)
. This call is pushed onto the stack, and the first call remains unfinished. 9
is less than10
, so9
is printed to the screen, and the method returns.- Control returns to the previous call where
n = 99
. Now,x
is set to99 % 10
, which is9
, and9 & 1
evaluates to1
. This1
is printed to the screen.
Try repeating this process with the other test calls listed above. Check that you understand why the output for these calls is:
Enigma(4)
prints4
Enigma(99)
prints9, 1
Enigma(123)
prints1, 0, 1
Enigma(-3456)
prints3, 0, 1, 0
Enigma(1234567)
prints1, 0, 1, 0, 1, 0, 1
When to Use Recursion
This brings us to the question: Which implementation is better? Is recursion always the best solution compared to an iterative one?
Recursion can simplify code and make certain problems easier to solve, especially those involving complex data structures like trees. However, it also uses more memory due to the overhead of multiple function calls and can lead to stack overflow if not carefully managed.
It's not the best solution for all problems, but for some cases (as we will see later in the chapter), recursion is precisely the right tool to use.
-
The stack is an area of memory that stores local variables and function calls. It follows a Last In, First Out (LIFO) order, meaning the last method pushed onto the stack is the first one to be popped off. ↩