Getting Started
While loops provide a powerful way to repeat tasks, some problems are more naturally solved by breaking them into smaller, self-similar subproblems. This chapter introduces recursion, a programming technique where a method calls itself to solve a problem. We will explore how this "divide and conquer" strategy is used to implement two highly efficient algorithms: binary search and merge sort.
What You Should Be Able to Do
Trace the execution of a recursive method, tracking its parameters and return values.
Identify the base case and recursive step in a recursive algorithm.
Implement a recursive binary search on a sorted array.
Trace the splitting and merging steps of the merge sort algorithm.
Understand how a missing or incorrect base case leads to a
StackOverflowError.
Key Concepts & Java Implementation
The Core Idea
Recursion is a problem-solving approach where the solution to a problem depends on solutions to smaller instances of the same problem. In Java, this is achieved when a method calls itself. Think of it like a set of Russian nesting dolls: to open the whole set, you open the largest doll, which contains a slightly smaller doll, and you repeat this process until you reach the smallest doll that cannot be opened.
Every well-formed recursive algorithm has two essential parts:
Base Case: The simplest possible instance of the problem, which can be solved directly without further recursion. This is the condition that stops the recursive calls. In the nesting doll analogy, this is the smallest, solid doll.
Recursive Step: The part of the algorithm that breaks the problem down into a smaller piece and calls the method on that smaller piece. This step must move the problem closer to the base case with each call.
This strategy is often called divide and conquer. The problem is divided into subproblems, the subproblems are solved (conquered) recursively, and then their solutions are combined if necessary.
Syntax & Implementation
Recursion doesn't introduce new Java keywords, but rather a new pattern of method invocation. The key is a method calling itself with modified arguments that progress toward the base case.
Annotated Java Example: Recursive Binary Search
Binary search is a fast algorithm for finding a target value within a sorted array. It works by repeatedly dividing the search interval in half.
public class Searcher {
/**
* Searches for a target value in a sorted array using recursion.
* @param arr The sorted array to search in.
* @param target The value to search for.
* @param low The starting index of the current search interval.
* @param high The ending index of the current search interval.
* @return The index of the target, or -1 if not found.
*/
public static int binarySearch(int[] arr, int target, int low, int high) {
// Base Case: If the search interval is invalid, the target is not in the array.
if (low > high) {
return -1;
}
// Find the middle index of the current interval.
int mid = low + (high - low) / 2;
// Recursive Step: Compare the middle element with the target.
if (arr[mid] == target) {
// Base Case: The target is found at the middle index.
return mid;
} else if (arr[mid] > target) {
// The target must be in the left half. Make a recursive call on the left subarray.
return binarySearch(arr, target, low, mid - 1);
} else {
// The target must be in the right half. Make a recursive call on the right subarray.
return binarySearch(arr, target, mid + 1, high);
}
}
public static void main(String[] args) {
int[] sortedData = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
// Initial call to start the search on the entire array (indices 0 to 9).
int result = binarySearch(sortedData, 23, 0, sortedData.length - 1);
System.out.println("Found at index: " + result); // Output: Found at index: 5
}
}
Annotated Java Example: Merge Sort
Merge sort is an efficient, recursive sorting algorithm. It divides an array into two halves, recursively sorts them, and then merges the two sorted halves back together.
import java.util.Arrays;
public class Sorter {
/**
* The main method that initiates the merge sort process.
* @param arr The array to be sorted.
*/
public static void mergeSort(int[] arr) {
// A helper method is often used to pass array indices.
mergeSort(arr, 0, arr.length - 1);
}
/**
* The recursive method that divides the array and calls the merge helper.
* @param arr The array to be sorted.
* @param left The starting index of the segment to sort.
* @param right The ending index of the segment to sort.
*/
private static void mergeSort(int[] arr, int left, int right) {
// Base Case: If the segment has 0 or 1 elements, it's already sorted.
if (left < right) {
// Find the middle point to divide the array into two halves.
int mid = left + (right - left) / 2;
// Recursive Step: Sort the first and second halves.
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// Merge the sorted halves.
merge(arr, left, mid, right);
}
}
/**
* Merges two sorted subarrays into one sorted array.
* Subarrays are arr[left...mid] and arr[mid+1...right].
*/
private static void merge(int[] arr, int left, int mid, int right) {
// Create temporary arrays to hold the two halves.
int[] leftArr = Arrays.copyOfRange(arr, left, mid + 1);
int[] rightArr = Arrays.copyOfRange(arr, mid + 1, right + 1);
int i = 0, j = 0; // Initial indices of first and second subarrays
int k = left; // Initial index of merged subarray in original array
// Merge the temp arrays back into the original array arr[left...right]
while (i < leftArr.length && j < rightArr.length) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
// Copy remaining elements of leftArr[], if any
while (i < leftArr.length) {
arr[k] = leftArr[i];
i++;
k++;
}
// Copy remaining elements of rightArr[], if any
while (j < rightArr.length) {
arr[k] = rightArr[j];
j++;
k++;
}
}
}
Tracing & Analysis
Execution Trace: Binary Search
Let's trace binarySearch(sortedData, 23, 0, 9) where sortedData is {2, 5, 8, 12, 16, 23, 38, 56, 72, 91}.
| Call # | low | high | mid | arr[mid] | Action |
|---|---|---|---|---|---|
| 1 | 0 | 9 | 4 | 16 | 16 < 23, search right half. Call binarySearch(..., 5, 9) |
| 2 | 5 | 9 | 7 | 56 | 56 > 23, search left half. Call binarySearch(..., 5, 6) |
| 3 | 5 | 6 | 5 | 23 | 23 == 23, target found. Return mid (which is 5). |
Analysis: Merge Sort
Merge sort's power comes from the merge step. Dividing the array is fast, but the real work happens when merging two already sorted lists. Because they are sorted, we can merge them in a single pass by simply comparing the front elements of each list and picking the smaller one. This process is highly efficient. While more complex to write than simpler sorts like selection sort, merge sort's performance on large datasets is significantly better.
Java Syntax Quick-Reference
This topic uses existing Java syntax in a new, recursive pattern.
methodName(newArgs): A method calling itself is the core of recursion. The argumentsnewArgsmust be different from the original call to move closer to a base case.if (baseCaseCondition): Anifstatement used to check for the base case and stop the recursion, typically by returning a value directly.return methodName(newArgs): In a method that returns a value, the recursive call is often part of thereturnstatement. This passes the result from the deepest call all the way back to the initial call.
Core Code Examples & Terminology
Recursion: The process in which a method calls itself one or more times in its body, either directly or indirectly, to solve a problem.
Base Case: A condition within a recursive method that stops the recursion. It represents the simplest version of the problem that can be solved without further recursive calls.
Recursive Step: The part of a recursive method that calls itself, but with modified parameters that move the problem closer to the base case.
Divide and Conquer: An algorithmic paradigm where a problem is broken down into smaller, similar subproblems, which are solved recursively, and their solutions are combined to solve the original problem.
Core Snippet 1 (Recursive Call Logic):
if (arr[mid] > target) { // The recursive step moves closer to the base case by reducing the search range. return binarySearch(arr, target, low, mid - 1); }This code demonstrates a recursive step where the method calls itself with an updated
highboundary.Core Snippet 2 (Base Case Logic):
if (low > high) { // The base case is met when the search range is empty. return -1; // Indicates the target was not found. }This code shows a base case in a binary search that stops recursion when the element cannot be found.
Core Snippet 3 (Merge Sort Division):
if (left < right) { int mid = left + (right - left) / 2; mergeSort(arr, left, mid); // Recurse on left half mergeSort(arr, mid + 1, right); // Recurse on right half merge(arr, left, mid, right); }This snippet shows the "divide" part of merge sort, where the array is split and the method recursively calls itself on both halves.
Core Skill Check
Code Tracing: What is the output of
mystery(3)?public int mystery(int n) { if (n == 0) return 1; return n * mystery(n - 1); }Answer:
6(It calculates 3 * 2 * 1 * 1)Debugging: Identify the error in this recursive code that causes it to fail.
public void countDown(int num) { System.out.println(num); countDown(num - 1); // The method never stops calling itself. }Answer: There is no base case. This will cause a
StackOverflowErroras the method calls itself infinitely.Application: Write a single line of Java code for the base case of a recursive method
sum(int n)that calculates the sum of integers from 1 ton.Answer:
if (n <= 1) return n;orif (n == 1) return 1;
Common Misconceptions & Errors
Infinite Recursion: Forgetting a base case or writing a recursive step that doesn't move closer to the base case will cause a
StackOverflowError. This error means the program has run out of memory for tracking method calls.Forgetting to
returna Result: In a recursive method that returns a value (like binary search), you mustreturnthe result of the recursive call (e.g.,return binarySearch(...)). Forgetting thereturnkeyword is a common mistake.Incorrect Parameter Updates: In the recursive step, passing the wrong arguments can lead to incorrect results or infinite recursion. For example, in binary search, passing
midinstead ofmid - 1ormid + 1can cause the search to get stuck.Off-by-One Errors: When calculating indices like
mid,low, orhigh, it's easy to be off by one, which can exclude a necessary element from the search or cause anArrayIndexOutOfBoundsException.
Summary
Recursion is a powerful technique for solving problems that can be broken down into smaller, self-similar versions. Every recursive method requires a base case to stop the recursion and a recursive step that moves the problem toward the base case. We explored two classic "divide and conquer" algorithms: recursive binary search, which efficiently finds elements in a sorted array, and merge sort, which provides a consistently fast way to sort data. Mastering recursion involves understanding how to trace the flow of method calls and ensuring that every path eventually leads to a terminating base case.