Binary Search


Overview

Binary search repeatedly discards half of a search space. The familiar version searches a sorted array, but the same idea works whenever the possible answers have a monotonic property: once a condition becomes true, it stays true for every larger answer, or vice versa.

A binary search solution usually runs in O(log n) time and uses O(1) extra space. The difficult part is not calculating the midpoint; it is defining an invariant that makes every boundary update correct. Before writing the loop, decide:

  • What does the search interval represent?
  • Is the interval closed ([left, right]) or half-open ([left, right))?
  • When the midpoint satisfies the condition, can it still be the answer?
  • Does the problem require any matching index, the first match, the last match, or the smallest feasible value?

Use left + (right - left) / 2 instead of (left + right) / 2 in Java to avoid integer overflow. After every iteration, the remaining interval must be strictly smaller.

Templates

The following three forms cover most binary search problems:

  • Exact match

    • Start with the closed interval left = 0, right = n - 1.
    • While left <= right, calculate mid.
    • If values[mid] == target, return mid.
    • If values[mid] < target, discard [left, mid] with left = mid + 1.
    • Otherwise, discard [mid, right] with right = mid - 1.
    • If the interval becomes empty, return -1.
  • Lower bound: first index whose value is at least the target

    • Search the half-open interval [left, right), initially [0, n).
    • While left < right, calculate mid.
    • If values[mid] < target, set left = mid + 1.
    • Otherwise, keep mid as a possible answer with right = mid.
    • Return left; it may equal n when no qualifying element exists.
  • Binary search on an answer

    • Identify an inclusive numeric range [low, high] containing every possible answer.
    • Define feasible(candidate) so its result changes monotonically across that range.
    • While low < high, calculate mid.
    • If feasible(mid) is true, keep mid with high = mid.
    • Otherwise, discard it with low = mid + 1.
    • Return low, the smallest feasible answer.
import java.util.function.IntPredicate;

public class BinarySearchTemplates {
	public static int binarySearch(int[] values, int target) {
		int left = 0;
		int right = values.length - 1;

		while (left <= right) {
			int mid = left + (right - left) / 2;

			if (values[mid] == target) {
				return mid;
			}
			if (values[mid] < target) {
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}

		return -1;
	}

	public static int lowerBound(int[] values, int target) {
		int left = 0;
		int right = values.length;

		while (left < right) {
			int mid = left + (right - left) / 2;

			if (values[mid] < target) {
				left = mid + 1;
			} else {
				right = mid;
			}
		}

		return left;
	}

	public static int firstFeasible(int low, int high, IntPredicate feasible) {
		while (low < high) {
			int mid = low + (high - low) / 2;

			if (feasible.test(mid)) {
				high = mid;
			} else {
				low = mid + 1;
			}
		}

		return low;
	}
}

Problems

34. Find First and Last Position of Element in Sorted Array

Given a non-decreasing array, return the first and last indices containing the target, or [-1, -1] when it is absent. Use the same bisect_left implementation twice: search for target to find its first position, then search for target + 1 and subtract one to find its last position. The runtime is O(log n).

class Solution {
	public int[] searchRange(int[] nums, int target) {
		int N = nums.length;
		if (N == 0) {
			return new int[] {-1, -1};
		}

		int idx1 = binarySearch(nums, target);
		if (idx1 == N || nums[idx1] != target) {
			return new int[] {-1, -1};
		}

		int idx2 = binarySearch(nums, 1 + target);
		return new int[] {idx1, idx2 - 1};
	}

	// implementation of bisect_left
	private int binarySearch(int[] nums, int target) {
		int left = 0;
		int right = nums.length; // NOT nums.length - 1 !!!

		while (left < right) {
			int mid = (left + right) / 2;

			if (nums[mid] < target) {
				left = mid + 1;
			} else {
				right = mid;
			}
		}

		return left;
	}
}

33. Search in Rotated Sorted Array

A sorted array of distinct values has been rotated at an unknown pivot. First binary search for the smallest value, which identifies the rotation point and divides the array into two sorted segments. Then run lower-bound search on the left segment and, if needed, the right segment. The three binary searches still take O(log n) time with O(1) extra space.

class Solution {
	public int search(int[] nums, int target) {
		int N = nums.length;
		int left = 0;
		int right = N - 1;

		while (left < right) {
			int mid = (left + right) / 2;
			if (nums[mid] > nums[N - 1]) {
				left = mid + 1;
			} else {
				right = mid;
			}
		}

		// when there is no rotation, left is 0 and the
		// left-segment search is automatically skipped
		int lMin = 0;
		int lMax = left - 1;
		int rMin = left;
		int rMax = N - 1;

		while (lMin < lMax) {
			int lMid = (lMin + lMax) / 2;
			if (nums[lMid] < target) {
				lMin = lMid + 1;
			} else {
				lMax = lMid;
			}
		}

		if (nums[lMin] == target) {
			return lMin;
		}

		while (rMin < rMax) {
			int rMid = (rMin + rMax) / 2;
			if (nums[rMid] < target) {
				rMin = rMid + 1;
			} else {
				rMax = rMid;
			}
		}

		return nums[rMin] == target ? rMin : -1;
	}
}

300. Longest Increasing Subsequence

Maintain res[i - 1] as the smallest possible ending value of an increasing subsequence of length i. For each number, use lower-bound search to find the first ending value greater than or equal to it. Replace that ending value, or append the number when it extends the longest subsequence. This takes O(n log n) time and O(n) space.

import java.util.ArrayList;
import java.util.List;

class Solution {
	public int lengthOfLIS(int[] nums) {
		// res[i - 1]: smallest possible ending value of an
		// increasing subsequence of length i
		List<Integer> res = new ArrayList<>();
		res.add(nums[0]);

		for (int i = 1; i < nums.length; i++) {
			int idx = lowerBound(res, nums[i]);
			if (idx == res.size()) {
				res.add(nums[i]);
			} else {
				res.set(idx, nums[i]);
			}
		}

		return res.size();
	}

	private int lowerBound(List<Integer> res, int target) {
		int left = 0;
		int right = res.size();

		while (left < right) {
			int mid = left + (right - left) / 2;
			if (res.get(mid) < target) {
				left = mid + 1;
			} else {
				right = mid;
			}
		}

		return left;
	}
}

875. Koko Eating Bananas

Choose the smallest integer eating speed that lets Koko finish every pile within h hours. A speed is either too slow or feasible, and every speed above a feasible one is also feasible. Binary search the answer range from 1 to the largest pile and use the required number of hours as the monotonic feasibility check. The runtime is O(n log m), where m is the largest pile.

class Solution {
	public int minEatingSpeed(int[] piles, int h) {
		int left = 1;
		int right = 0;

		for (int p : piles) {
			right = Math.max(right, p);
		}

		while (left < right) {
			int mid = (left + right) / 2;

			if (canFinish(piles, h, mid)) {
				right = mid;
			} else {
				left = mid + 1;
			}
		}

		return left;
	}

	private boolean canFinish(int[] piles, int h, int k) {
		long total = 0;

		for (int p : piles) {
			total += (p + (long) k - 1) / k;
		}

		return total <= h;
	}
}

1283. Find the Smallest Divisor Given a Threshold

Binary search the divisor from 1 through max(nums). As the divisor increases, the sum of the rounded-up quotients never increases, so isBelowThreshold is a monotonic predicate. Keep the feasible half and return the first divisor that satisfies the threshold. If M = max(nums), the runtime is O(n log M) with O(1) extra space.

class Solution {
	public int smallestDivisor(int[] nums, int threshold) {
		int dMin = 1;
		int dMax = 0;

		for (int n : nums) {
			dMax = Math.max(dMax, n);
		}

		while (dMin < dMax) {
			int dMid = (dMin + dMax) / 2;

			if (isBelowThreshold(nums, threshold, dMid)) {
				dMax = dMid;
			} else {
				dMin = 1 + dMid;
			}
		}

		return dMin;
	}

	private boolean isBelowThreshold(int[] nums, int threshold, int d) {
		long total = 0;

		for (int n : nums) {
			total += (n + d - 1) / d;
		}

		return total <= threshold;
	}
}

410. Split Array Largest Sum

Binary search the maximum allowed subarray sum from max(nums) through sum(nums). For each candidate, greedily count how many subarrays are needed when no subarray may exceed that sum. A larger candidate can only require the same number or fewer subarrays, so feasibility is monotonic. The runtime is O(n log(sum(nums))) with O(1) extra space.

class Solution {
	public int splitArray(int[] nums, int k) {
		int minS = 0;
		int maxS = 0;

		for (int num : nums) {
			minS = Math.max(minS, num);
			maxS += num;
		}

		while (minS < maxS) {
			int midS = minS + (maxS - minS) / 2;

			if (checkSum(nums, k, midS) == false) {
				minS = midS + 1;
			} else {
				maxS = midS;
			}
		}

		return minS;
	}

	private boolean checkSum(int[] nums, int k, int target) {
		int count = 0;
		int subTotal = 0;

		for (int num : nums) {
			subTotal += num;
			if (subTotal == target) {
				count++;
				subTotal = 0;
			} else if (subTotal > target) {
				count++;
				subTotal = num;
			} else {
				continue;
			}
		}

		if (subTotal > 0) {
			count++;
		}

		return count <= k;
	}
}

4. Median of Two Sorted Arrays

Find the median without merging the arrays. Binary search the last index taken from the shorter array and derive the corresponding index in the other array. Use negative and positive infinity at partition boundaries. When both left values are no greater than the opposite right values, the partition is valid and directly yields the median. This takes O(log(min(m, n))) time and O(1) extra space.

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int N1 = nums1.length;
        int N2 = nums2.length;
        if (N1 > N2){
            return findMedianSortedArrays(nums2, nums1);
        }

        int L = (N1 + N2)/2;
        int left = -1;
        int right = N1 - 1;

        while (true){
            int mid1 = (left + right)/2;
            int mid2 = L - mid1 - 2;

            int n1_left = (mid1 >= 0) ? nums1[mid1] : Integer.MIN_VALUE;
            int n1_right = (mid1 + 1 < N1) ? nums1[mid1+1] : Integer.MAX_VALUE;
            int n2_left = (mid2 >= 0) ? nums2[mid2] : Integer.MIN_VALUE;
            int n2_right = (mid2 + 1 < N2) ? nums2[mid2+1] : Integer.MAX_VALUE;

            if (n1_left <= n2_right && n2_left <= n1_right){
                if ( (N1 + N2) % 2 == 1){
                    return Math.min(n1_right, n2_right);
                } else {
                    return 0.5*(Math.max(n1_left, n2_left)+Math.min(n1_right, n2_right));
                }
            } else if (n1_left > n2_right){
                right = mid1 - 1;
            } else {
                left = mid1 + 1;
            }
        }
    }
}