Skip to main content

Section 26.3 Recursive Searches

Any algorithm that involves iteration can be written recursively. The key as always with recursion is to:
  • Identify the base case(s) that will stop the recursion.
  • Identify how to do β€œone step” in the general case that moves the recursion closer to the base case.
  • Decide what parameters the recursive function needs to keep track of the current state.

Subsection 26.3.1 Linear Search Recursively

To implement a linear search recursively we can look at the first element of the list. If it is the key, return its index. If not, recursively search the rest of the items. To do this job, we need to know the list, the key we are looking for, and the current index we are checking.
  • Base cases: reach the end of the list or find the key.
  • One step: if the current index is not the key, recursively move to the next index.
  • Parameters: the list, the key, and the current index.
Listing 26.3.1.
template<typename T>
int linearSearchRecursive(const vector<T>& vec, T key, int currentIndex = 0) {
    if (currentIndex >= vec.size()) {
        // Reached the end without finding the key
        return -1; 
    } else if (vec.at(currentIndex) == key) {
        // Found the key at the current index
        return currentIndex;
    } else {
        // Not found yet, check the next index, return the result
        return linearSearchRecursive(vec, key, currentIndex + 1);
    }
}
Lines 2 and 5 are the two base cases: either we have found the key at the current index, or we have checked all indexes without finding it. If neither base case is met, we recursively move on to the next index in line 10.
Note that the currentIndex parameter has a default value of 0. So we can make a call to the function without specifying that parameter to start searching from the beginning of the list.

Note 26.3.1.

Another way to implement the recursive linear search is to reduce the size of the list being searched with each recursive call. Check the first item of the list, and if it is not the key, make a recursive call with a sublist that excludes the first item. For example, if we started with the list {10, 20, 30, 40, 50} and did not find what we were looking for at index 0, we would make a recursive call with the list {20, 30, 40, 50}.
Making new lists is much less efficient than just keeping track of the current index. If we start with a list of \(n\) items using that approach, we would create a list of size \(n-1\) for the first recursive call, then a list of size \(n-2\) for the next call, and so on down to size 1. The total amount of work creating all those lists would be proportional to \(n^2\text{.}\) Which is significantly more work than the work we want to do with the search itself, which is proportional to \(n\text{.}\)

Subsection 26.3.2 Binary Search Recursively

Although any iteration can be turned into recursion, some algorithms are more naturally expressed recursively than others. Binary search is an algorithm that has a very natural recursive structure. How do you do binary search on a list of items? You check the middle item and if it is not what you are looking for, you do binary search on either the lower half or the upper half of the list.
Listing 26.3.2.
Again, there are two base cases that can end the recursion. But, there are also two recursive cases. Either we can recursively search the bottom half of the remaining items, or we can recursively search the top half.
For this function, we use a non-recursive β€œwrapper” function to kickstart the recursion. When main calls binarySearch(numbers, key), that function calls the recursive function with indexes 0 and size - 1. We could skip that helper by having main pass those indexes directly to the recursive function. But having the wrapper function makes it easier to use.

Checkpoint 26.3.1.

We have a vector numbers with the values 6, 10, 17, 21, 26, 30, 37, 40, 49, 54.
A vector containing 6, 10, 17, 21, 26, 30, 37, 40, 49, 54.
Figure 26.3.3. The numbers vector.
We make a call to do a binary search for the value 37. Build the call stack that will be created by the recursive calls.
The top function call should be the non-recursive wrapper function. The bottom function call should be the recursive call that finds the value 37.
You will not use all of the blocks.
Hint.
This is the same vector used in ActivityΒ 26.2.1. You can use that activity as a reference.

Checkpoint 26.3.2.

Suppose you have the following sorted list {3, 5, 6, 8, 11, 12, 14, 15, 17, 18} and are using the recursive binary search algorithm. Which group of numbers correctly shows the sequence of comparisons used to search for the key 16?
  • 12, 15, 17
  • The size is 9. When you divide that by 2, you get 4.5, which rounds down to 4. That is the first index to check.
  • 11, 14, 17
  • In the second step, your range should include {12, 14, 15, 17, 18}. What is the middle of that range?
  • 12, 15, 17
  • The size is 9. When you divide that by 2, you get 4.5, which rounds down to 4. That is the first index to check.
  • 11, 15, 17
  • Binary search starts at the midpoint and halves the list each time. It is done when the start index is greater than the end index.
You have attempted of activities on this page.