// STUHUB · C++ DATA STRUCTURES
Binary Search: Invariants, Off-By-One Bugs, and the Exact Loop That Never Fails
Why binary search is easy to state and hard to write: the loop invariant, the three classic off-by-one patterns, and a version of the algorithm you can reproduce under exam pressure.
Introduction
Binary search finds a target in a sorted array in O(log n) by halving the search space each step. The idea takes one line to state; the implementation is famously bug-prone — Jon Bentley's classic exercise found that most professional programmers could not write it correctly. This note pins down the invariant that makes the loop provably correct, shows the three off-by-one patterns that break it, and ends with drills you can do before an exam.
The invariant that makes the loop correct
Every correct binary search maintains one invariant: if the target exists, it lies within [lo, hi] — the current, shrinking candidate range. The loop continues while the range is non-empty (lo <= hi); each iteration either finds the target or discards a half in a way that preserves the invariant; and when the range empties, the target is provably absent.
The subtle requirement is on the midpoint update. If arr[mid] < target, the target — if present — must be to the right of mid, so the new range starts at mid + 1. If arr[mid] > target, it must be to the left including nothing at mid, so the range ends at mid - 1. Getting +1/-1 wrong does not crash; it silently discards the answer or loops forever.
int binary_search(const int arr[], int n, int target) {
int lo = 0, hi = n - 1; // candidate range [lo, hi], inclusive
while (lo <= hi) { // range non-empty?
int mid = lo + (hi - lo) / 2; // overflow-safe midpoint
if (arr[mid] == target) return mid;
if (arr[mid] < target) lo = mid + 1; // discard [lo, mid]
else hi = mid - 1; // discard [mid, hi]
}
return -1; // range empty: not present
}The three classic bugs (and how each smells)
Bug 1 — missing +1/-1: lo = mid or hi = mid with the inclusive range. With a two-element range where mid rounds to lo, the range never shrinks and the loop spins forever. Smell: infinite loop on tiny inputs; test [1, 2] searching for 2.
Bug 2 — wrong loop condition: while (lo < hi) with lo <= hi-style updates. This exits before examining the last candidate. Smell: target at an endpoint is reported missing; test [5] searching for 5.
Bug 3 — overflow midpoint: (lo + hi) / 2 can overflow when both indices are large ints. Smell: works in tests, undefined behavior on huge arrays. The fix is lo + (hi - lo) / 2, or in C++20, std::midpoint(lo, hi).
The two variants exams actually ask for
Exact-match search (above) is the warm-up. The two variants that matter in exams and interviews:
Lower bound — the first index whose value is >= target. The invariant changes to "everything left of lo is < target":
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] < target) lo = mid + 1; // arr[mid] can't be the answer
else hi = mid; // arr[mid] might be the answer
}
return lo; // first index with arr[lo] >= targetNote the asymmetric update (hi = mid, no -1) matching the half-open convention — mixing the two conventions in one loop is bug pattern #1 in disguise. In production code, std::lower_bound implements exactly this.
Search in a rotated sorted array — decide which half is sorted by comparing endpoints, then check whether the target lies in the sorted half. Same loop skeleton, one extra branch.
// lower_bound: first index with arr[index] >= target
int lower_bound_idx(const int arr[], int n, int target) {
int lo = 0, hi = n; // half-open [lo, hi)
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo; // may be n: all values < target
}Why O(log n) — the counting argument
Each iteration halves the candidate range: n → n/2 → n/4 → ... → 1. The number of halvings until the range has one element is ⌈log₂ n⌉, so at most that many iterations, each O(1). For n = 1,000,000 that is ~20 comparisons; for n = 10⁹, ~30. This is also the exam favorite: "how many comparisons for n = 2²⁰?" — answer: at most 21 (20 halvings plus the final comparison).
The precondition is easy to state and easy to forget: the array must be sorted for the discard step to be valid. Running binary search on unsorted data returns garbage without any error — it is the algorithm's quietest failure mode.
Exercises with solutions
Work through each question before opening the solution below it.
Exercise 1
Trace binary_search on arr = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] with target = 23. Write down lo, hi, mid at each step.
Solution
Step 1: lo=0, hi=9, mid=4 → arr[4]=16 < 23 → lo=5. Step 2: lo=5, hi=9, mid=7 → arr[7]=56 > 23 → hi=6. Step 3: lo=5, hi=6, mid=5 → arr[5]=23 == target → return 5. Three comparisons; the invariant held throughout (23 always inside [lo, hi]).
Exercise 2
A classmate's binary search infinite-loops on arr = [1, 3], target = 3. Their update is lo = mid. Diagnose and fix.
Solution
Trace: lo=0, hi=1, mid=0 → arr[0]=1 < 3 → lo = mid = 0. The range never changes: infinite loop. The bug: with lo <= hi (inclusive range), discarding the left half must skip mid, because arr[mid] was already examined and is not the target. Fix: lo = mid + 1. General rule: in an inclusive-range loop, both updates move strictly past mid.
Exercise 3
Write count_occurrences(arr, n, target) for a sorted array with duplicates, in O(log n), using lower/upper bounds.
Solution
Count = (index of first element > target) − (index of first element >= target).
int count_occ(const int arr[], int n, int target) {
int lo = 0, hi = n; // lower bound: first >= target
while (lo < hi) {
int m = lo + (hi - lo) / 2;
if (arr[m] < target) lo = m + 1; else hi = m;
}
int first_ge = lo;
hi = n; // upper bound: first > target
while (lo < hi) {
int m = lo + (hi - lo) / 2;
if (arr[m] <= target) lo = m + 1; else hi = m;
}
return lo - first_ge; // occurrences of target
}Both loops share the same midpoint idiom lo + (hi - lo) / 2 — writing them symmetrically is itself a bug guard. (In real code, prefer std::equal_range.)
Published and updated 2026-08-09 · DUOCODE TECHNOLOGY