Binary search is not a sorted-array search algorithm. It’s an algorithm for finding the boundary in a monotone predicate — and most of its real uses are not on arrays at all.
The mental reframing
Stop thinking “find element in sorted array.” Start thinking: “I have a predicate p(x) that is false for small x and true for large x (or vice versa). Find the boundary.”
Examples that don’t look like binary search until you reframe:
- Koko eating bananas. Predicate: “can finish all piles in
hhours at speedk?” Monotone ink. Search over[1, max(piles)]. - Capacity to ship within D days. Predicate: “can pack into D groups with max sum
cap?” Monotone incap. - Median of two sorted arrays. Search the partition point in the smaller array; the predicate is “is the partition correct?”
- Allocating books / split array largest sum. Same shape — minimize the max chunk subject to a count constraint.
The template that doesn’t bug
Pick one and use it everywhere. The “find leftmost true” template:
lo, hi = lower_bound, upper_bound + 1 # half-open
while lo < hi:
mid = (lo + hi) // 2
if predicate(mid):
hi = mid
else:
lo = mid + 1
return lo # smallest x where predicate is true
Half-open intervals + lo < hi + lo = mid+1 / hi = mid is the combination that doesn’t infinite-loop on adjacent indices. Most binary-search bugs are people improvising the boundary conditions per problem.
On a sorted 2D matrix
If rows and columns are both sorted: walk from top-right (or bottom-left). Each step rules out a row or a column. O(m + n). Don’t reach for binary search here — the linear walk is cleaner and the same complexity class.
If the matrix is row-major sorted (each row strictly greater than the previous), treat as a 1D array of length m·n and binary-search once.
Where binary search does NOT apply
The predicate has to be monotone. If p(x) is true for some isolated x and false elsewhere, you don’t have binary search — you have linear scan or a different structure.