In the first two lessons of this chapter , we learned about searching and sorting algorithms that using iteration (loops) to search or sort arrays and ArrayLists. And as we saw in the previous lesson, recursion can also be used to traverse Strings, arrays, and ArrayLists but is even more useful for traversing tree structures.
In this lesson, we will take a look at how we can use recursion to implement binary searching and a recursive sorting algorithm called merge-sort algorithm.
In SubsectionΒ 12.1.3Β Binary search we saw how to implement a binary search using a loop. And in the previous lesson we learned that recursion, while it can be used for linear traversals, is most powerful as βloops for treesβ. Letβs look at another way of thinking about binary search that lends itself naturally to a recursive solution.
public class BinarySearch {
public static int search(int[] nums, int n) {
int low = 0;
int high = nums.length;
while (low < high) {
int mid = low + (high - low) / 2;
if (nums[mid] < n) {
low = mid + 1;
} else if (nums[mid] > n) {
high = mid;
} else {
return mid;
}
}
return -1;
}
}
Now letβs consider how that algorithm proceeds. Letβs assume weβre searching an array with length 8. So the low and high variables will start at 0 and 8, representing the half-open interval \([0, 8)\text{.}\) And the first value of mid will be 4. On the next iteration of the loop low and high will be either 0 and 4 or 4 and 8, depending on which half of the original range the target value is determined to be in. And then whichever range we end up in, on the next iteration it will be split in half one one of itβs halves chosen and so on until we get down to ranges containing just a single number.
While in any given call to search we will go down one path until we find, or fail to find, our target number. But we could also think of the possible paths as a tree that starts with one range covering the whole range and then has children that represent each half, and each of them have children representing their two halves until we reach the leaves containing only one number each. We can draw the tree like this.
With that tree in mind, we can write a recursive version of binary search. Unlike our iterative binary search method, the recursive search needs to take two extra arguments that specify what part of the array any single call is responsible for. The first call will pass a range covering the whole array and then each recursive calls will narrow the range to one half of the range. In other words, to find a number in a given range we determine which half of that range itβs in and then find in in that smaller range. Eventually the range will get small enough that we either found it or we know itβs not there.
What are the base cases for a recursive version of binary search? Hint, look at the tree above and think about when we can no longer go any deeper into the tree. There are two such cases.
Given a recursive binary search method with the method signature βboolean binarySearch(int[] array, int startIndex, int endIndex, int target)β, what recursive method call would search the array from index 0 to the middle index?
Now weβre ready to look at an implementation. This version is designed to exactly match the structure of our tree diagram above. It could be optimized a bit to return immediately if the target value is ever found at the midpoint.
Note that binary search is one of many algorithms that can be written either using loops or recursively. Which you prefer when is largely a matter of taste though all other things being equal, in many languages iteration is preferred and likely to be more efficient. On the other hand, for some algorithms, it can be very straightforward to express them recursively and very difficult to translate them into iterative code.
In SectionΒ 12.2Β Sorting algorithms, we looked at two sorting algorithms, selection sort and insertion sort. In this lesson, we will look at a third sorting algorithm, merge sort, which uses recursion. In this case recursion makes the algorithm more efficient than either of the other two algorithms weβve looked it.
Similar to how the recursive binary search worked by searching smaller and smaller sub-ranges of the original array, merge sort repeatedly divides ranges covering part of the array into smaller sub-ranges until each range is just one element wide and then recursively merges adjacent ranges back together in sorted order.
For example, after four adjacent one-element ranges are merged into two adjacent two-element ranges, those two-element ranges are merged into one four-element range, and so on until on the very last step, the sorted first half of the array is merged with the sorted second half of the array, completing the sort.
Merge sort typically uses a second array the same size as the original array to hold the values while merging and then copies the sorted values back into the original array.
And here is an implementation of merge sort. Note that the only public method here is sort but it is written in terms of two helper methods: sortRange and merge. The sortRange method is actually the recursive method. Since recursive methods often need some extra arguments to specify what sub-problem each recursive call is working on, itβs common to write a non-recursive method that calls a recursive method with the right initial arguments to kick things off. Here sort calls sortRange with the range covering the whole array.
To understand merge sort it can be useful to trace through the steps using parentheses or curly braces to show how the array is divided into sub-arrays and then merged. For example, here is a trace of of sorting the array {86, 3, 43, 5} from the code above.
Subsection12.4.3Tracing Challenge: Recursive Search and Sort
Working in pairs, practice the recursive binary search and merge sort algorithms with a deck of cards or pieces of paper with numbers or names on them. Hereβs a video that shows merge sort with cards.
Trace through recursiveBinarySearch(sortedArray, 0, 8, 22) looking for the target number 22 where sortedArray = {2, 5, 8, 10, 11, 15, 17, 20, 22}. Write down each middle element that is checked and the start and end index for each recursive call. How many elements did the binary search have to check before finding 22? How would this compare to a linear search?
(AP 4.17.B.1) Data must be in sorted order to use the binary search algorithm. Binary search starts at the middle of a sorted array or ArrayList and eliminates half of the array or ArrayList in each recursive call until the desired value is found or all elements have been eliminated.
(AP 4.17.C.2) Merge sort repeatedly divides an array into smaller sub-arrays until each sub-array is one element and then recursively merges the sorted sub-arrays back together in sorted order to form the final sorted array.
You can now do the following review lessons about recursion at the end of the unit and College Board Progress Check for Unit 4 Part 3 in the AP Classroom. The rest of this unit consists of the reviews for each section of the unit.