Recursive Searching and Sorting
Why this matters
Imagine you're in the school library, looking for a single definition in a massive, 1,500-page dictionary. You could start at page one and flip through every single page until you find your word. That's called a linear search, and it works, but it's slow and painful. What if you're looking for the word "recursive"? You know it's in the 'R' section, somewhere in the back half of the book.
So, you open the dictionary right to the middle. You land in the 'M's. You know 'R' comes after 'M', so you can completely ignore the entire first half of the book. You just eliminated 750 pages in one move. Now you take the back half and open it to the middle... and so on.
This "divide and conquer" approach is the heart of what we're learning today. We'll explore two powerful, recursive algorithms that use this same logic: Binary Search, for finding things in a sorted list, and Merge Sort, for sorting a jumbled list in the first place.
Concept overview
flowchart TD
A[Start: Sorted Array, Target] --> B{low > high?};
B -- Yes --> C[Not Found. Return -1];
B -- No --> D[mid = (low + high) / 2];
D --> E{arr[mid] == Target?};
E -- Yes --> F[Found! Return mid];
E -- No --> G{Target < arr[mid]?};
G -- Yes --> H[Search Left Half: high = mid - 1];
G -- No --> I[Search Right Half: low = mid + 1];
H --> B;
I --> B;
Core explanation
Hey everyone, it's Saavi. Let's dive into two of the most elegant ideas in computer science. They might feel a little abstract at first, but once you see the pattern, you'll appreciate how clever and powerful they are.
Recursion on Collections
Before we get to the big algorithms, let's quickly refresh how recursion works on data collections like String objects or arrays. Remember, a recursive solution has two parts:
- 1Base CaseThe simplest possible problem that can be solved directly, which stops the recursion.
- 2Recursive StepThe part that breaks the problem into a smaller version of itself and calls the method again on that smaller piece.
For example, you could write a recursive method to check if a word is a palindrome. The base case might be a word with 0 or 1 letters (which is always a palindrome). The recursive step would check if the first and last letters match, and if they do, you'd recursively call the method on the substring without those letters. This idea of breaking a collection down is fundamental to what follows.
Binary Search: The Smartest Guessing Game
Binary search is your go-to algorithm for finding an element in a sorted collection. I can't stress that enough.
The Golden Rule: Binary search only works on sorted data. If you try to run it on an unsorted array of numbers from a basketball game's stats, you'll get incorrect results or miss the value entirely. Why? Because the whole algorithm relies on being able to eliminate half the data based on a comparison, just like with the dictionary example.
Here's how it works on a sorted array:
- 1Find the middleYou look at the element right in the middle of the array.
- 2Compare
- Is this the element you're looking for? If yes, you're done! That's a base case.
- Is your target value smaller than the middle element? If yes, you know it must be in the left half. You can completely ignore the right half.
- Is your target value larger than the middle element? If yes, you know it must be in the right half. You can ignore the left half.
- 3RecurseYou repeat the process on the remaining half of the array.
This process continues until you either find the element or the search space runs out (meaning the element isn't in the array). Each step cuts your search space in half, which makes it incredibly fast—much more efficient than checking every single element one by one (linear search).
While you can write binary search with a while loop (iteratively), the recursive version is a beautiful demonstration of a "divide and conquer" strategy.
// A conceptual look at recursive binary search
public int binarySearch(int[] arr, int target, int low, int high) {
if (low > high) {
return -1; // Base case: not found
}
int mid = low + (high - low) / 2; // Avoids potential overflow
if (arr[mid] == target) {
return mid; // Base case: found!
} else if (target < arr[mid]) {
// Recursive step: search the left half
return binarySearch(arr, target, low, mid - 1);
} else {
// Recursive step: search the right half
return binarySearch(arr, target, mid + 1, high);
}
}
Merge Sort: Divide, Conquer, and Combine
What if your data isn't sorted? You need an efficient way to sort it. Enter Merge Sort.
Merge sort is a recursive sorting algorithm that is famous for its reliability and efficiency. It follows the "divide and conquer" mantra perfectly.
That's the Divide step. Merge sort recursively splits the array in half until it's broken down into a series of one-element arrays.
[8, 3, 5, 1] -> [8, 3] and [5, 1] -> [8], [3], [5], [1]
Now comes the Conquer (or Merge) step. This is the real work. Starting with those single-element arrays, you merge them back together in sorted order.
- You take two adjacent "piles" (which are already sorted) and a new, empty "pile".
- You look at the top form of each pile. Whichever comes first alphabetically, you move it to your new, merged pile.
- You repeat this, always comparing the top forms of your two source piles, until one pile is empty.
- Then, you just add the rest of the remaining pile to the end of your new pile.
You repeat this merging process up the chain. The two-form piles get merged into sorted four-form piles, which get merged into sorted eight-form piles, and so on, until you have one, big, perfectly sorted stack of forms.
Merge sort is a workhorse. It's more predictable than other sorting algorithms you might see later, and its efficiency is a benchmark for sorting.
See it in action
Worked examples
Let's trace these algorithms so you can see them in action. Tracing is a critical skill for the AP exam.
Binary Search Trace
Problem: Your friend Priya gives you a sorted list of her top scores from an arcade game: [120, 155, 210, 280, 350, 490, 510, 600]. Use binary search to find out if she ever scored 210.
We'll keep track of the low index, the high index, and the mid index. Initially, low = 0 and high = 7.
Iteration 1:
- Array portion
[120, 155, 210, 280, 350, 490, 510, 600] low = 0,high = 7- Calculate middle
mid = (0 + 7) / 2 = 3(integer division). - CompareThe element at index 3 is
280. - DecisionOur target
210is less than280. So, we know our answer (if it exists) must be in the left half. We discard the middle element and everything to its right. - Next stepOur new search space is from index
0tomid - 1, which is2. So,lowstays0,highbecomes2.
Iteration 2:
- Array portion
[120, 155, 210] low = 0,high = 2- Calculate middle
mid = (0 + 2) / 2 = 1. - CompareThe element at index 1 is
155. - DecisionOur target
210is greater than155. We discard the middle element and everything to its left. - Next stepOur new search space is from
mid + 1tohigh. So,lowbecomes2,highstays2.
Iteration 3:
- Array portion
[210] low = 2,high = 2- Calculate middle
mid = (2 + 2) / 2 = 2. - CompareThe element at index 2 is
210. - DecisionThis is our target! We found it at index 2. The algorithm returns
2.
Merge Sort Trace
Problem: Sort the following array of customer wait times (in minutes) at a popular Boston food truck: [9, 4, 7, 2].
Step 1: The "Divide" Phase The algorithm recursively splits the array until each element is in its own subarray.
[9, 4, 7, 2]is split into[9, 4]and[7, 2].[9, 4]is split into[9]and[4].[7, 2]is split into[7]and[2].
Now we have our base cases: four arrays of size one. [9], [4], [7], [2].
Step 2: The "Merge" Phase Now we merge them back together in sorted order.
-
Merge
[9]and[4]:- Compare
9and4.4is smaller. - The merged result is
[4, 9].
- Compare
-
Merge
[7]and[2]:- Compare
7and2.2is smaller. - The merged result is
[2, 7].
- Compare
-
Merge
[4, 9]and[2, 7]:- This is the most important step. We have two sorted lists and we'll create a new one.
- Compare the first elements:
4and2.2is smaller. Our new list is[2]. - The lists are now
[4, 9]and[7]. - Compare the first elements:
4and7.4is smaller. Our new list is[2, 4]. - The lists are now
[9]and[7]. - Compare the first elements:
9and7.7is smaller. Our new list is[2, 4, 7]. - The lists are now
[9]and[]. The second list is empty. - We just add the remaining elements from the first list (
9). - The final merged result is
[2, 4, 7, 9].
The array is now sorted!
Try it yourself
Ready to try it yourself? Grab a piece of paper and trace the logic.
Problem 1: Binary Search Practice
You have a sorted ArrayList of product IDs from a warehouse in Dallas: [101, 115, 132, 247, 301, 302, 450, 489, 512].
Trace the steps of a recursive binary search to find the product with ID 450. Keep track of the low, high, and mid indices for each recursive call. What are they for the second recursive call?
Problem 2: Merge Sort Practice
You're given an array of unsorted scores from a school's soccer tryouts: [88, 75, 92, 81, 79, 95].
Show the result of the first merge operation that would happen after the array is completely split. Then, show the result of merging [75, 88] and [81, 92] together.
Practice — 8 questions
In simple terms, recursive searching and sorting is about using a "divide and conquer" strategy to find items in a list or to put a list in order, like a super-efficient librarian.
// A conceptual look at recursive binary search
public int binarySearch(int[] arr, int target, int low, int high) {
if (low > high) {
return -1; // Base case: not found
}
int mid = low + (high - low) / 2; // Avoids potential overflow
if (arr[mid] == target) {
return mid; // Base case: found!
} else if (target < arr[mid]) {
// Recursive step: search the left half
return binarySearch(arr, target, low, mid - 1);
} else {
// Recursive step: search the right half
return binarySearch(arr, target, mid + 1, high);
}
}
- 4.17.A: Determine the result of executing recursive algorithms that use strings or collections.
- 4.17.B: Determine the result of each iteration of a binary search algorithm used to search for information in a collection.
- 4.17.C: Determine the result of each iteration of the merge sort algorithm when used to sort a collection.
- 4.17.A.1
- Recursion can be used to traverse String objects, arrays, and ArrayList objects.
- 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.
- 4.17.B.2
- Binary search is typically more efficient than linear search.
- 4.17.B.3
- The binary search algorithm can be written either iteratively or recursively.
- 4.17.C.1
- Merge sort is a recursive sorting algorithm that can be used to sort elements in an array or ArrayList.
- 4.17.C.2
- Merge sort repeatedly divides an array into smaller subarrays until each subarray is one element and then recursively merges the sorted subarrays back together in sorted order to form the final sorted array.
flowchart TD
A[Start: Sorted Array, Target] --> B{low > high?};
B -- Yes --> C[Not Found. Return -1];
B -- No --> D[mid = (low + high) / 2];
D --> E{arr[mid] == Target?};
E -- Yes --> F[Found! Return mid];
E -- No --> G{Target < arr[mid]?};
G -- Yes --> H[Search Left Half: high = mid - 1];
G -- No --> I[Search Right Half: low = mid + 1];
H --> B;
I --> B;
Read what Saavi narrates
(upbeat, warm music fades in and then fades to background)
Hey everyone, it's Saavi from Shrutam.
Imagine you're in a huge library, looking for a single definition in a massive, 1,500-page dictionary. You could start at page one and flip through every single page... but that's slow. What if you opened the book right to the middle? You land in the 'M's. If you're looking for the word 'recursive', you know it's in the back half. You just eliminated 750 pages in one move.
That smart "divide and conquer" idea is exactly what we're talking about today. We're learning how recursion can help us search and sort data incredibly fast. We’ll focus on two big algorithms: Binary Search for finding things, and Merge Sort for organizing things.
Let's walk through an example of binary search. It's a fantastic tool, but it has one golden rule: the data must already be sorted.
Okay, imagine your friend Priya has a sorted list of her top arcade scores: 120, 155, 210, 280, and so on. We want to see if she ever scored exactly 210.
First, we look at the whole list and find the middle element. Let's say that's 280. We ask, is our target, 210, bigger or smaller than 280? It's smaller. So, we know we only have to look at the first half of the list. We just threw away the entire second half.
Now, we repeat the process on that smaller, leftover list. We find its middle element, which is 155. Our target, 210, is bigger than 155. So this time, we throw away the first part of this smaller list.
We're left with just one number to check... and it's 210! We found it. See how quickly we zeroed in on the answer by cutting the problem in half each time? That’s the power of binary search.
Now, a common mistake here is forgetting that the list must be sorted. If the scores were all jumbled up, you couldn't make those confident decisions to throw away half the data. The whole method would fall apart. So, always remember: binary search requires sorted data.
These recursive ideas... they're like puzzles. Take your time with them, trace them out on paper, and you'll see the elegant pattern behind them. You can do this.
(music swells and fades out)
Binary search's logic depends entirely on the data being ordered. If you search for `10` in `[20, 5, 10, 30]`, it might check the middle (`10`), find it, and stop. But if you search for `5`, it will check `10`, then look in the "left" half (`[20]`) and fail, even though `5` is in the array.
Always confirm the collection is sorted before applying binary search. If it's not, you must sort it first (perhaps with merge sort!).
If you set `high = mid`, you are including the middle element in the next search space. But you've already checked the middle element and know it's not the target. This can lead to an infinite loop if the target is not found.
When the target is smaller than `arr[mid]`, the new boundary is `high = mid - 1`. When it's larger, the new boundary is `low = mid + 1`. You are explicitly excluding the `mid` you just checked.
The "divide" phase does no sorting whatsoever. It only breaks the array down into smaller and smaller pieces. All the sorting logic happens during the "merge" phase when the pieces are being put back together.
Remember the two distinct phases: Divide (split) and Conquer (merge and sort). The sorting happens on the way back up.
When merging two subarrays, one will run out of elements first. If you stop there, you will lose the remaining elements from the other subarray, resulting in an incomplete and incorrect final array.
After your main comparison loop finishes, always have code that copies any remaining elements from whichever subarray is not yet empty into the final merged array.