Binary search eliminates half the search space with every comparison β O(log n) search on sorted data, and the foundation for 'find the boundary' problems on monotonic functions.
Algorithms
Binary Search Fundamentals
Halve the search space with every comparison β O(log n) on sorted data. Binary search goes far beyond "find target in array" β it finds boundaries, minimises maximums, and solves any problem where the answer space is monotonic.
Binary search works on any sorted or monotonic structure: compare the middle element to the target, then discard the half that cannot contain the answer. Each step eliminates 50% of the remaining candidates, giving O(log n) time β 20 comparisons to search 1 million elements, 30 comparisons for 1 billion. The prerequisite is order: the data must be sorted, or the predicate must be monotonic (false-false-false-true-true-true). Binary search shows up in three patterns in interviews: (1) classic "find target in sorted array," (2) lower/upper bound β "find the first/last position of target," and (3) "binary search on the answer" β the answer space is monotonic and you binary search for the optimal value. Pattern 3 is the hardest to recognise but the most frequently tested at the medium/hard level. The biggest source of bugs is off-by-one errors in the loop condition and boundary updates β always use the lo < hi template with lo = mid + 1 or hi = mid to avoid infinite loops.
Analogy: The number-guessing game. "I'm thinking of a number between 1 and 100." You guess 50. "Too high." Now you know the answer is 1β49. Guess 25. "Too low." Now 26β49. Each guess eliminates half the range. In 7 guesses (logβ 100 β 7) you find any number β not 50 guesses with linear scan. Binary search IS this game, applied to any ordered search space.
02
Section Two Β· Mental Model
How It Thinks
Array is sorted β elements to the left of mid are β€ mid, elements to the right are β₯ mid
βΆ
A single comparison with arr[mid] eliminates an entire half β search space shrinks from n to n/2 to n/4 ... to 1 in log n steps
Mid is calculated as lo + (hi - lo) / 2 instead of (lo + hi) / 2
βΆ
Prevents integer overflow when lo + hi exceeds Integer.MAX_VALUE β a classic bug in every language with fixed-size integers
The loop shrinks the range [lo, hi] until lo == hi (or lo > hi)
βΆ
When the loop exits, lo (or hi) points to the answer boundary β either the target itself or the insertion point
Lower bound: "find the first index where arr[i] β₯ target"
βΆ
When arr[mid] < target β lo = mid + 1 (answer is to the right); when arr[mid] β₯ target β hi = mid (mid might be the answer, don't skip it)
Binary search on the answer: the answer space [min, max] is monotonic β feasible(x) flips from false to true at some threshold
βΆ
Binary search finds that threshold without checking every value β O(log(max-min) Γ cost_of_feasibility_check) total
Off-by-one errors come from inconsistent lo/hi updates and loop conditions
βΆ
Use ONE consistent template: while (lo < hi) with lo = mid + 1 and hi = mid β the loop terminates with lo == hi, the boundary
03
Section Three Β· Classic Search
Classic Binary Search β Find Exact Target
Find Target in Sorted Array
Return the index of target in a sorted array, or -1 if not found.
Pseudocode
functionbinarySearch(arr, target):
lo = 0, hi = arr.length - 1while lo <= hi:
mid = lo + (hi - lo) / 2if arr[mid] == target:
return mid
else if arr[mid] < target:
lo = mid + 1else:
hi = mid - 1return -1// O(log n)
The classic template uses lo <= hi with hi = mid - 1:
This three-way comparison (equal/less/greater) returns as soon as the target is found.
If the loop exits without finding the target, return -1.
This template works ONLY for exact-match search β for boundary-finding, use the lo < hi template in the next section.
04
Section Four Β· Bounds
Lower Bound & Upper Bound
Lower bound finds the first index where arr[i] β₯ target (leftmost insertion point). Upper bound finds the first index where arr[i] > target (rightmost insertion point). Together, they give you the range [lowerBound, upperBound) of all occurrences of target. This is the foundation of "Find First and Last Position of Element" (LC 34) and is more powerful than the classic search β it handles duplicates, insertion points, and boundary-finding in one consistent template.
Lower Bound
First index where arr[i] β₯ target
intlowerBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] < target)
lo = mid + 1; // too smallelse
hi = mid; // could be answer
}
return lo; // first i: arr[i]β₯target
}
Upper Bound
First index where arr[i] > target
intupperBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] <= target)
lo = mid + 1; // β€ is too smallelse
hi = mid; // could be answer
}
return lo; // first i: arr[i]>target
}
Lower and Upper Bound on [1, 3, 5, 5, 5, 7, 9] with target = 5
The lo < hi template is the universal boundary finder:
The only thing that changes between lower bound and upper bound is the comparison β < target vs <= target.
When the loop exits, lo == hi and points to the boundary.
This single template handles: first occurrence, last occurrence, insertion point, and "search on answer" problems.
05
Section Five Β· Boundary Pattern
The Universal Boundary Template
Every binary search problem can be reduced to: "find the first index where condition(mid) is true." The condition splits the search space into [false, false, ..., false, true, true, ..., true]. Binary search finds the boundary between false and true. This abstraction unifies all binary search variants β exact match, lower bound, upper bound, rotated array search, and "search on answer" β into a single template.
The Universal Template
// Find FIRST index where condition(mid) is TRUE// Search space: [false, false, ..., false, TRUE, true, ..., true]// β this is what we findint lo = MIN_POSSIBLE, hi = MAX_POSSIBLE;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (condition(mid))
hi = mid; // mid might be the answer β keep itelse
lo = mid + 1; // mid is definitely not β skip it
}
return lo; // lo == hi == first true index
Applying the Template
Problem
condition(mid)
lo
hi
Lower bound (first β₯ target)
arr[mid] >= target
0
n
Upper bound (first > target)
arr[mid] > target
0
n
First occurrence of target
arr[mid] >= target
0
n
Last occurrence of target
arr[mid] > target β result - 1
0
n
Min speed to finish (LC 875)
canFinish(mid)
1
max(piles)
Min capacity to ship (LC 1011)
canShip(mid, days)
max(weights)
sum(weights)
The mental trick β reduce to yes/no:
Don't think "where is the target?" Think "at what point does the answer CHANGE from no to yes?" Binary search finds that transition point.
If you can formulate your problem as a monotonic boolean function, binary search applies β even when there's no array at all.
06
Section Six Β· Search on Answer
Binary Search on the Answer
The hardest-to-recognise binary search pattern: the answer itself is a number in a range [min, max], and there's a monotonic feasibility function β if answer x works, then x+1 also works (or vice versa). Instead of trying every possible answer linearly, binary search the answer space. The key insight: you're NOT searching an array β you're searching a range of possible answers, using a helper function to check "is this answer feasible?"
Example: Koko Eating Bananas (LC 875)
Koko eats bananas at speed k (bananas/hour). Given piles of bananas and h hours, find the minimum k such that she finishes all piles within h hours.
Pseudocode
functionminEatingSpeed(piles, h):
lo = 1// minimum speed
hi = max(piles) // maximum speed (eat biggest in 1h)while lo < hi:
mid = lo + (hi - lo) / 2ifcanFinish(piles, mid, h): // can she finish at speed mid?
hi = mid // yes β try slowerelse:
lo = mid + 1// no β must go fasterreturn lo // O(n Γ log(max))functioncanFinish(piles, speed, h):
hours = 0for each pile in piles:
hours += ceil(pile / speed) // hours needed for this pilereturn hours <= h
Binary search on answer β monotonic feasibility
Signal for "binary search on the answer":
The problem asks to "minimize the maximum" or "find the minimum value such that ..." β and there's a yes/no feasibility check.
If you can write a function isFeasible(x) where the result is monotonic (all false then all true, or vice versa), binary search on x.
07
Section Seven Β· Pitfalls
Common Pitfalls & How to Avoid Them
The Four Deadliest Bugs
Bug
Cause
Fix
Integer overflow
(lo + hi) / 2 overflows when lo + hi > INT_MAX
Use lo + (hi - lo) / 2
Infinite loop
lo = mid when mid = lo + (hi-lo)/2 rounds down β lo never advances
Always use lo = mid + 1 in the lo < hi template. If you need hi = mid - 1, use mid = lo + (hi-lo+1)/2 (round up)
Off-by-one in hi
Using hi = n - 1 vs hi = n β the answer might be "past the array"
For lower/upper bound, set hi = n (past-the-end is a valid answer meaning "no element qualifies")
Wrong template
Using lo <= hi for boundary search or lo < hi for exact match
Exact match: lo <= hi + three-way if. Boundary: lo < hi + two-way if
Common Mistake: Mixing templates. The while (lo <= hi) template with hi = mid - 1 finds an exact match. The while (lo < hi) template with hi = mid finds a boundary. Using hi = mid in the lo <= hi loop causes an infinite loop. Pick one template and stick with it β the lo < hi boundary template covers all cases.
08
Section Eight Β· Implementation
Build It Once
Build all three variants (classic, lower bound, upper bound) once β and the "search on answer" pattern. In interviews, the lower bound template covers 90% of binary search problems.
Java β Binary Search core mechanics
// Classic β exact match β O(log n)intbinarySearch(int[] arr, int target) {
int lo = 0, hi = arr.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
// Lower bound β first i where arr[i] >= target β O(log n)intlowerBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo;
}
// Upper bound β first i where arr[i] > target β O(log n)intupperBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] <= target) lo = mid + 1;
else hi = mid;
}
return lo;
}
Binary Search β Full Implementation
Java β All binary search variants
import java.util.*;
public classBinarySearch {
// βββ 1. Classic β exact match ββββββββββββ O(log n)staticintsearch(int[] arr, int target) {
int lo = 0, hi = arr.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
// βββ 2. Lower bound β first i: arr[i]β₯t β O(log n)staticintlowerBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo;
}
// βββ 3. Upper bound β first i: arr[i]>t β O(log n)staticintupperBound(int[] arr, int target) {
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] <= target) lo = mid + 1;
else hi = mid;
}
return lo;
}
// βββ 4. First and last position ββββββββββ O(log n)staticint[] searchRange(int[] arr, int target) {
int first = lowerBound(arr, target);
if (first == arr.length || arr[first] != target)
return newint[]{-1, -1};
int last = upperBound(arr, target) - 1;
return newint[]{first, last};
}
// βββ 5. Search in rotated sorted array βββ O(log n)staticintsearchRotated(int[] arr, int target) {
int lo = 0, hi = arr.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
if (arr[lo] <= arr[mid]) { // left half sortedif (arr[lo] <= target && target < arr[mid])
hi = mid - 1;
else lo = mid + 1;
} else { // right half sortedif (arr[mid] < target && target <= arr[hi])
lo = mid + 1;
else hi = mid - 1;
}
}
return -1;
}
// βββ 6. Find minimum in rotated array ββββ O(log n)staticintfindMin(int[] arr) {
int lo = 0, hi = arr.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] > arr[hi]) lo = mid + 1;
else hi = mid;
}
return arr[lo];
}
// βββ 7. Koko Eating Bananas (search on answer) β O(n log max)staticintminEatingSpeed(int[] piles, int h) {
int lo = 1, hi = 0;
for (int p : piles) hi = Math.max(hi, p);
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
long hours = 0;
for (int p : piles)
hours += (p + mid - 1) / mid; // ceil(p/mid)if (hours <= h) hi = mid; // can finish β try slowerelse lo = mid + 1; // too slow β speed up
}
return lo;
}
// βββ 8. Search a 2D Matrix βββββββββββββββ O(log(mΓn))staticbooleansearchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int lo = 0, hi = m * n - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int val = matrix[mid / n][mid % n];
if (val == target) return true;
else if (val < target) lo = mid + 1;
else hi = mid - 1;
}
return false;
}
// βββ Main βββββββββββββββββββββββββββββββββββpublic static voidmain(String[] args) {
int[] arr = {1,3,5,5,5,7,9};
System.out.println("Search 5: " + search(arr, 5)); // 3System.out.println("Lower 5: " + lowerBound(arr, 5)); // 2System.out.println("Upper 5: " + upperBound(arr, 5)); // 5System.out.println("Range 5: "
+ Arrays.toString(searchRange(arr, 5))); // [2,4]int[] rot = {4,5,6,7,0,1,2};
System.out.println("Rotated 0: " + searchRotated(rot, 0)); // 4System.out.println("Min: " + findMin(rot)); // 0int[] piles = {3,6,7,11};
System.out.println("Koko(8h): " + minEatingSpeed(piles, 8)); // 4
}
}
Python β Binary Search implementations
import math
# βββ 1. Classic β exact match βββββββββββ O(log n)defbinary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if arr[mid] == target: return mid
elif arr[mid] < target: lo = mid + 1
else: hi = mid - 1
return -1
# βββ 2. Lower bound β first i: arr[i]>=t O(log n)deflower_bound(arr, target):
lo, hi = 0, len(arr)
while lo < hi:
mid = (lo + hi) // 2
if arr[mid] < target: lo = mid + 1
else: hi = mid
return lo
# βββ 3. Upper bound β first i: arr[i]>t β O(log n)defupper_bound(arr, target):
lo, hi = 0, len(arr)
while lo < hi:
mid = (lo + hi) // 2
if arr[mid] <= target: lo = mid + 1
else: hi = mid
return lo
# βββ 4. First and last position ββββββββββ O(log n)defsearch_range(arr, target):
first = lower_bound(arr, target)
if first == len(arr) or arr[first] != target:
return [-1, -1]
last = upper_bound(arr, target) - 1
return [first, last]
# βββ 5. Search in rotated array βββββββββββ O(log n)defsearch_rotated(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target: return mid
if arr[lo] <= arr[mid]:
if arr[lo] <= target < arr[mid]: hi = mid - 1
else: lo = mid + 1
else:
if arr[mid] < target <= arr[hi]: lo = mid + 1
else: hi = mid - 1
return -1
# βββ 6. Koko Eating Bananas βββββββββββββββ O(n log max)defmin_eating_speed(piles, h):
lo, hi = 1, max(piles)
while lo < hi:
mid = (lo + hi) // 2
hours = sum(math.ceil(p / mid) for p in piles)
if hours <= h: hi = mid
else: lo = mid + 1
return lo
# βββ Python built-in: bisect ββββββββββββββimport bisect
# bisect.bisect_left(arr, x) β lower bound# bisect.bisect_right(arr, x) β upper bound# bisect.insort(arr, x) β insert maintaining order# Demo
arr = [1,3,5,5,5,7,9]
print("Search 5:", binary_search(arr, 5)) # 3
print("Lower 5:", lower_bound(arr, 5)) # 2
print("Upper 5:", upper_bound(arr, 5)) # 5
print("Range 5:", search_range(arr, 5)) # [2, 4]
print("Koko:", min_eating_speed([3,6,7,11], 8)) # 4
09
Section Nine Β· Java Reference
Use It In Java
IN JAVA β Arrays.binarySearch() and Collections.binarySearch()
ArrayArrays.binarySearch(arr, target) β returns index or -(insertion point) - 1
ListCollections.binarySearch(list, target) β same return semantics
// Basic search β returns index or negativeint idx = Arrays.binarySearch(arr, target);
if (idx < 0) {
int insertionPoint = -(idx + 1); // where it would be inserted
}
// Search in a rangeint idx = Arrays.binarySearch(arr, fromIndex, toIndex, target);
// β Java does NOT have lowerBound/upperBound built-in// You must write them yourself for: // - first/last occurrence// - count of elements in range// - search on answer problems// TreeMap/TreeSet have floor/ceiling which act like boundsTreeMap<Integer, String> map = newTreeMap<>();
map.floorKey(target); // largest key β€ target
map.ceilingKey(target); // smallest key β₯ target
map.lowerKey(target); // largest key < target
map.higherKey(target); // smallest key > target
Method
Returns
Note
Arrays.binarySearch(arr, x)
index or -(insertPt)-1
Array MUST be sorted first
Collections.binarySearch(list, x)
index or -(insertPt)-1
List MUST be sorted
TreeMap.floorKey(x)
largest key β€ x
O(log n) β like lower bound
TreeMap.ceilingKey(x)
smallest key β₯ x
O(log n) β like lower bound
β Gotcha:Arrays.binarySearch() does NOT guarantee which index is returned when there are duplicates β it just returns "an" index of the target, not the first or last. For "find first/last occurrence," you MUST write your own lower/upper bound. Don't rely on the built-in for boundary problems.
ChooseUse Arrays.binarySearch() for simple "is target present?" checks. Write your own lower/upper bound for all interview problems β the 5-line template is easy to memorise and covers every binary search variant.
10
Section Ten Β· Practice
Problems To Solve
Binary search interview problems fall into three tiers: (1) classic search in a sorted array β easy, test template mastery; (2) modified search β rotated arrays, 2D matrices, peak finding β medium, test edge-case handling; (3) binary search on the answer β "minimise the maximum," "find minimum speed/capacity" β hardest to recognise, test problem-modelling ability. Tier 3 is where most candidates fail β not because the code is hard, but because they don't realise binary search applies.
At each step, one half is sorted. Check if target falls in the sorted half β if yes, search there; if no, search the other half. Key: compare arr[lo] with arr[mid] to find the sorted half.
If arr[mid] < arr[mid+1], a peak exists to the right (go right). Otherwise go left. The binary search converges on a peak in O(log n). No need for sorted input β just monotonic local property.
Binary search on speed [1, max(piles)]. For each speed, check if Koko can finish in h hours. Monotonic: higher speed β fewer hours. Find minimum speed where hours β€ h.
Binary search on capacity [max(weights), sum(weights)]. For each capacity, greedily count ships needed. Monotonic: higher capacity β fewer ships. Find minimum capacity for β€ days ships.
Interview Pattern:
Every binary search problem reduces to "find the boundary where condition changes from false to true." For array problems, the condition involves arr[mid] vs target.
For "search on answer" problems, the condition is a feasibility function you write. Same template, different condition.
Master the lo < hi template with lo = mid + 1 / hi = mid β it solves every variant.