Algorithm B: Binary Search (Lower Bound)
TAOCP Vol. 3, §6.2.1
Given a sorted array $a$ of size $n$ and a value $target$, find the leftmost insertion point: the smallest index $lo$ such that every element at index $\geq lo$ is $\geq target$. Equivalently:
- If $target$ is present in $a$, return the index of its first occurrence.
- If $target$ is absent, return the index at which it would be inserted to keep $a$ sorted.
This is the shape of C++'s std::lower_bound and Python's bisect.bisect_left.
Why lower bound, not exact match. Knuth's Algorithm B in TAOCP §6.2.1 is the textbook exact-match form: return the index of some occurrence of $target$, or report "not found." Pedagogically clean. But it falls apart on the moment interviewers care about most: duplicates. Given $a = [1, 2, 2, 2, 3]$ and $target = 2$, exact-match binary search might land on index 1, 2, or 3 depending on how ties are broken. Lower bound always returns the leftmost — index 1 — which is what you need when the problem says "the first occurrence," "the number of values strictly less than $target$," or "where would I insert $target$."
The same lower-bound shape generalizes far beyond array search. Any monotone predicate $f$ admits a binary search: "find the smallest $k$ such that $f(k)$ is true." Specialize $f(i) = (a[i] \geq target)$ and you have lower bound on an array; specialize $f(c) = (\text{can finish in capacity } c)$ and you have the capacity binary search family (Koko eating bananas, ship-within-D-days, splitting an array into K subarrays). The half-open window and the asymmetric narrowing — lo = mid + 1 vs hi = mid — are the same in every variant.
The half-open window. Notice that $lo$ is inclusive but $hi$ is exclusive: we search $a[lo..hi)$, not $a[lo..hi]$. This is what makes the narrowing rules in B4 symmetric. Writing hi = mid (rather than hi = mid - 1) keeps $mid$ as a candidate when $a[mid] = target$, while still strictly shrinking the window because $mid < hi$.
Overflow-safe midpoint. Computing mid = (lo + hi) / 2 is the textbook formula, but lo + hi can overflow a fixed-width integer when both indices grow large. The equivalent mid = lo + (hi - lo) / 2 avoids this. This is the bug that lived in java.util.Arrays.binarySearch for nine years — worth internalizing.
The trace below shows $lo$, $hi$, and $mid$ at every labeled state on the example $a = [1, 3, 5, 7, 9, 11, 13, 15]$, $target = 7$. Step through it: each iteration strictly contracts the window, and at termination $lo = hi$ marks the answer.
Computation Method
Why It Works
The invariant holds at B1: the window is the whole array. Each B4 narrowing preserves it. If $a[mid] < target$, sortedness rules out every index $\leq mid$, so $lo \leftarrow mid + 1$ is safe. If $a[mid] \geq target$, the target (if present) must lie at $mid$ or earlier, so $hi \leftarrow mid$ is safe — note that $mid$ stays *inside* the new window because $hi$ is exclusive. The window strictly shrinks each iteration (since $lo \leq mid < hi$ at B3), so the loop terminates in at most $\lceil \log_2 n \rceil + 1$ iterations. At termination, $lo = hi$ marks the partition point: every index $< lo$ has value $< target$ and every index $\geq lo$ has value $\geq target$. That is precisely the leftmost insertion point.
Interactive Trace
Step through a lower-bound binary search on a sorted array. The live window a[lo..hi) is tinted; mid is outlined at each comparison; the window strictly shrinks each iteration.