Recursion on an array
A method that calls itself is recursive. On an array, each call handles 1 position and then calls itself to handle the positions after it. The array is never copied. An index parameter marks the first position still to handle, and each call passes that index plus 1.
The call that returns without calling itself again is the base case. On an array, the base case is usually the index reaching the end of the array (i == array.length), where no positions are left.
| Case | When it runs | What it does |
|---|---|---|
| Base case | no positions left | returns an answer without another call |
| Recursive case | at least 1 position left | handles position i, then calls itself with i + 1 |
A recursion problem on the APE allows no loops. The practice exam's file says No loops of any form are allowed.
The recursive call has to do the repeating that a for, while or do loop would do.
Problems that work from both ends of the array at once use 2 indexes in place of 1; see 2 indexes moving toward each other.
Example: adding up an array
public static int sum(int[] array) {
return sum(array, 0);
}
private static int sum(int[] array, int i) {
if (i == array.length) {
return 0;
}
return array[i] + sum(array, i + 1);
}
The base case returns 0, because the sum of no numbers is 0. The recursive case adds the element at i to the sum of everything after it.
Trace it on {4, 7, 2}. Each call waits for the call below it to return, then adds its own element.
| Call | i | Returns |
|---|---|---|
sum(array, 0) | 0 | 4 + 9 = 13 |
sum(array, 1) | 1 | 7 + 2 = 9 |
sum(array, 2) | 2 | 2 + 0 = 2 |
sum(array, 3) | 3 | 0 (base case) |
3 elements make 4 calls: 1 for each element, plus the base case. The base case and the call stack shows how Java keeps track of the calls that are waiting.
The public method and the helper
A caller wants to pass only the array. The recursion also needs the index. So the work is split between 2 methods with the same name and different parameter lists (overloading).
- The public method takes the array, checks it (for example, throws
IllegalArgumentExceptionwhen it isnull), and makes the first call to the helper. That first call gives every helper parameter its starting value. - The helper, often
private, holds the base case and the recursive case.
Read the public method's call to the helper before you write the helper. The starting values tell you which positions are already handled and which are left. A first call can pass an element as the starting answer; that element is then already handled.
Carrying the answer down
The sum helper adds as each call returns. A helper can instead pass a partial answer down to the next call, in an extra parameter (an accumulator). The base case then returns the finished answer.
public static int countNegatives(int[] array) {
return countNegatives(array, 0, 0);
}
private static int countNegatives(int[] array, int i, int count) {
if (i == array.length) {
return count;
}
if (array[i] < 0) {
return countNegatives(array, i + 1, count + 1);
}
return countNegatives(array, i + 1, count);
}
On {-3, 8, -1} the calls receive a count of 0, 1, 1 and 2. The base case returns 2.
| Add as each call returns | Carry the answer down | |
|---|---|---|
| Helper parameters | array, i | array, i, count |
| First call | sum(array, 0) | countNegatives(array, 0, 0) |
| Base case returns | the answer for no elements (0 for a sum) | the carried answer |
| Where the work happens | after the recursive call returns | in the arguments of the recursive call |
A loop runs while i < array.length, so it is tempting to stop the recursion at i == array.length - 1. That stops 1 call early, and the last element is never counted. On {-3, 8, -1}, countNegatives would return 1.
The same base case breaks on an empty array. The index starts at 0 and array.length - 1 is -1, so the base case never runs, and array[0] throws ArrayIndexOutOfBoundsException.
Before you trust a helper, trace it by hand on an empty array, a 1 element array and a 2 element array.
2 indexes moving toward each other
Some problems work from both ends of the array at once. The helper takes 2 indexes: left starts at 0 and right starts at array.length - 1. Each call handles the pair at left and right, then calls itself with left + 1 and right - 1.
This method counts the pairs, 1 from each end, that add up to target.
public static int pairsAddingTo(int[] array, int target) {
return pairsAddingTo(array, target, 0, array.length - 1);
}
private static int pairsAddingTo(int[] array, int target, int left, int right) {
if (left >= right) {
return 0;
}
int found = 0;
if (array[left] + array[right] == target) {
found = 1;
}
return found + pairsAddingTo(array, target, left + 1, right - 1);
}
The base case is left >= right. It covers both ways the indexes can finish.
Array, target 10 | Pairs checked (left, right) | How it ends | Returns |
|---|---|---|---|
{1, 4, 6, 9}, even length | (0, 3): 1 + 9; (1, 2): 4 + 6 | left 2, right 1: the indexes crossed | 2 |
{2, 5, 7, 3, 8}, odd length | (0, 4): 2 + 8; (1, 3): 5 + 3 | left 2, right 2: the indexes met on the middle element | 1 |
{}, empty | none | left 0, right -1: crossed before any pair | 0 |
A base case of left == right catches only the odd case. On an even length the indexes skip past each other, the calls keep going, and an index runs off the array with ArrayIndexOutOfBoundsException.
In the practice APE
Practice exam 1 has 2 recursion problems, findLargest and reverseArray, in its Recursion folder. Practice exam 2 has findLargest and isPalindrome, in src/recursion. In each file the public method is given, and its last line is the first call to the helper you write. Read that call first. Your code goes between the //startMethod and //endMethod lines, and the practice grader checks that the method there calls itself and uses no for, while or do loop. Each findLargest file also says Failure to productively use recursion to solve this problem will result in 0 points.
Readiness check
Work each one on paper and write the answer down before you open the answers.
Use the sum methods on {3, 1, 4, 1, 5}.
- How many calls to the helper happen?
- What does each call return, from the last call to the first?
- What does
sum(new int[0])return, and how many helper calls does it make?
Answers
1. 6 calls: 1 for each of the 5 elements, plus the base case at i 5.
2. 0, then 5, 6, 10, 11, 14. The first call returns 14.
3. It returns 0 from 1 call. i is 0 and the length is 0, so the first call is the base case.
A public method you may not change reads:
public static int countAbove(int[] array, int limit) {
return countAbove(array, limit, array.length - 1);
}
The helper is private static int countAbove(int[] array, int limit, int i). It returns how many elements are greater than limit.
- Which position does the first call handle, and which way must
imove? - Write the base case and the recursive case.
- On an empty array, what is
ion the first call, and what comes back? - Someone writes the base case as
i == 0. What goes wrong on{9, 2}withlimit5, and on an empty array?
Answers
1. The last position, array.length - 1. Each call must pass i - 1, moving toward the front.
2. if (i < 0) { return 0; }, then if (array[i] > limit) { return 1 + countAbove(array, limit, i - 1); }, then return countAbove(array, limit, i - 1);
3. i is -1, so the first call is the base case and returns 0.
4. On {9, 2} the method stops at index 0 without checking it, so it returns 0 instead of 1. On an empty array i starts at -1 and never equals 0, so array[-1] throws ArrayIndexOutOfBoundsException.
Use the pairsAddingTo methods.
pairsAddingTo(new int[]{3, 9, 5, 5, 1, 7}, 10). List each (left,right) pair checked and give the result.- On a 7 element array, how many pairs are checked, and which index is never part of a pair?
- The base case is changed to
left == right. What happens on{1, 4, 6, 9}?
Answers
1. (0, 5): 3 + 7 is 10. (1, 4): 9 + 1 is 10. (2, 3): 5 + 5 is 10. Then left 3 and right 2 have crossed. The result is 3.
2. 3 pairs. Index 3, the middle, is never part of a pair: the indexes meet there and the base case returns.
3. The calls go (0, 3), (1, 2), (2, 1), (3, 0), (4, -1). The indexes never equal each other, so the base case never runs. The call with left 4 reads array[4] and throws ArrayIndexOutOfBoundsException.
Further reading
- Recursion (computer science) (Wikipedia). The general shapes, including the accumulator parameter.
- Reading 14: Recursion, MIT 6.031, Software Construction. In Java; its section on helper methods covers the public method and helper split.
- Binary search (Wikipedia). Another array recursion with 2 indexes; the range halves on each call, and the base case is the range running out.
- Programming Model, from Sedgewick and Wayne's Algorithms at Princeton. Arrays, and a recursion section on binary search.
- IllegalArgumentException (Java SE 19 & JDK 19). The exception a public method throws for an argument it cannot use.
- Arrays and Accumulator Patterns, from CSCD 210. The loop version of carrying a running answer.