Recursion in Java: Tracing and Algorithms
A progressive guide to understanding, tracing, designing, and comparing recursive algorithms in Java, including strings, arrays, binary search, and merge sort.
The Structure of
solves a problem by repeatedly reducing it to a smaller problem with the same structure. A correct recursive method has two essential parts: a and a .
The gives a result without making another call. The makes a and must change the problem so that the will eventually be reached. For a countdown, the arguments might be , , , and finally . At , the method stops instead of calling itself again.
For a factorial computation, the reduction can be written as
with the stopping rule . Thus, the computation for becomes .
Two checks are essential:
The method must have a reachable .
Every must move closer to that .
A method with no never stops. A method whose argument moves away from its also fails to terminate and may eventually cause a StackOverflowError.
Takeaway: Identify the smallest valid problem first, then verify that every makes that problem smaller or simpler.
Tracing Calls and Returns
Every method invocation creates a separate on the . When a recursive method calls itself, the current invocation pauses while the new invocation runs. This creates a downward sequence of calls followed by an upward sequence of returns.
Consider a method that returns the sum from through . A call with creates the sequence
The base call returns . The suspended calls then resume:
returns .
returns .
returns .
Each invocation has its own parameter value. The in the call with argument is distinct from the in the call with argument .
When tracing a method, write the calls downward until a is reached, then evaluate the suspended calls upward. Keep printed output separate from returned values. A statement before the executes while the calls are being made; a statement after the executes while the calls are returning. Therefore, printing before the call can produce , , , while printing after the call can produce , , .
Takeaway: Trace in two phases: follow calls toward the , then resolve returns in reverse order.
Recursive Processing of Strings and Arrays
Recursive methods can process one part of a string or array at a time and combine the result with the rest.
For a string, a common strategy is to inspect the first character and recursively process the remaining substring. For example, counting the character 'a' in "banana" examines each character in turn and reaches the empty string. The empty string is the natural because it contributes a count of .
A string reversal can remove the first character, reverse the remainder, and append the removed character at the end. The computation for "cat" follows this pattern:
A palindrome test compares the first and last characters. If they match, the method recursively checks the substring between them. A string of length at most is a palindrome, and a mismatch immediately produces false.
For an array, an index parameter identifies the portion that remains. An array-sum method can add the element at index to the sum beginning at index . When reaches the array length, no elements remain and the contribution is . For the values , , and , the result is .
The same pattern supports counting elements that satisfy a condition or finding a maximum. A maximum method must define what happens for an empty array before attempting to access an element.
Takeaway: For strings, shrink the substring; for arrays, advance an index or narrow a range. In both cases, define the empty or smallest input explicitly.
Divide-and-Conquer Algorithms
is especially useful when a problem repeatedly divides into smaller regions.
requires a sorted array. It examines the middle index, compares that value with the target, and recursively searches only the half that could still contain the target. For the sorted values 3, 8, 12, 17, 25, 31, 40 and target 25, the middle values are 17, then 31, then 25. The target is found at index . If the lower bound becomes greater than the upper bound, the range is empty and the search returns .
uses a different divide-and-conquer pattern:
Divide the collection into left and right portions.
Recursively sort both portions.
Merge the two sorted portions.
A portion containing zero or one element is already sorted. For the values 8, 3, 6, 2, the method sorts 8, 3 into 3, 8, sorts 6, 2 into 2, 6, and then merges them into 2, 3, 6, 8.
eliminates half of a search range at each step, whereas creates two recursive branches and later combines their results. When tracing either algorithm, record the current bounds or subranges carefully and check the boundary conditions.
Takeaway: narrows one sorted range; recursively sorts two halves and combines them.
Designing and Evaluating Recursive Solutions
A reliable way to design or analyze a recursive method is to answer five questions:
What is the smallest valid problem?
What condition identifies that problem?
What smaller problem does the solve?
How are the current result and recursive result combined?
What initial input and boundary conditions are required?
For an array-sum method, the smallest problem is that no elements remain. The base condition is that the index equals the array length. The smaller problem begins at the next index, the progress measure is an index increase of , and the combination is the current value plus the recursive sum.
Boundary conditions vary by algorithm:
An empty string may be the for character processing.
An array maximum method may require a nonempty array.
A search range is empty when the lower bound exceeds the upper bound.
A merge-sort portion is finished when it has zero or one element.
and iteration can express the same repetition. An iterative sum updates an accumulator inside a loop, while a recursive sum combines with the sum for . Iteration generally uses less call-stack memory for simple repetition, while can express divide-and-conquer or nested structures clearly.
When answering a tracing question, distinguish three different things: the arguments at each invocation, the order of printed output, and the value returned by each invocation. A branching method such as a recurrence with calls on and creates a call tree rather than a single chain.
Final checklist: Find the , verify progress, trace calls downward, resolve results upward, and inspect empty-input and boundary cases.