Problem Context
Binary search is the algorithm everyone "knows" and almost no one writes correctly on the first try. Jon Bentley reported in 1986 that 90% of professional programmers couldn't implement it without bugs after several hours; forty years later, the famous(low + high) / 2 overflow bug still shipped in java.util.Arrays.binarySearchuntil Joshua Bloch flagged it in 2006. The lesson isn't that binary search is hard โ it's that off-by-one errors, overflow, and unclear loop invariants compound viciously when you're halving an interval.
Modern interview and production work expects more than the textbook version. You need lower bound and upper boundvariants, "binary search on the answer" for optimization problems, and the discipline to pick a single loop invariant and stick with it. This guide is one mental model that works for all of them.
- You write
(low + high) / 2without thinking about overflow - You can't remember whether the loop is
while (low < high)or<= - You've hit an infinite loop because
low = midinstead ofmid + 1 - You don't know how to find the first occurrence vs any occurrence
This is one invariant โ half-open [low, high) โ that solves all four binary-search variants without bugs.
Concept Explanation
Pick one invariant and never deviate. The cleanest is the half-open interval [low, high):
lowis the smallest index that might be the answer.highis one past the largest index that might be the answer.- The search space size is
high - low; we stop when it's 0. - Every iteration shrinks the space by at least half โ O(log n) guaranteed.
flowchart LR
S["Start: low=0, high=n"] --> C{"low < high?"}
C -->|yes| M["mid = low + (high - low) / 2"]
M --> P{"predicate(mid)?"}
P -->|true| L["high = mid<br/>(answer โค mid)"]
P -->|false| H["low = mid + 1<br/>(answer > mid)"]
L --> C
H --> C
C -->|no| R["Result: low<br/>= first index where predicate is true"]
style M fill:#0078D4,color:#fff,stroke:#005a9e
style R fill:#16a34a,color:#fff,stroke:#15803d
Implementation
Step 1: The canonical lower-bound template
// Returns the smallest index in [0, n] where predicate(i) is true.
// If never true, returns n. Predicate must be monotonic:
// false, false, ..., false, true, true, ..., true
public static int LowerBound(int n, Func<int, bool> predicate)
{
int low = 0, high = n;
while (low < high)
{
int mid = low + (high - low) / 2; // โ overflow-safe
if (predicate(mid)) high = mid; // mid might be the answer
else low = mid + 1; // mid is definitely not
}
return low;
}Step 2: Search for an exact value (textbook)
public static int IndexOf(int[] arr, int target)
{
int idx = LowerBound(arr.Length, i => arr[i] >= target);
return (idx < arr.Length && arr[idx] == target) ? idx : -1;
}Step 3: First and last occurrence in O(log n)
public static (int first, int last) Range(int[] arr, int target)
{
int first = LowerBound(arr.Length, i => arr[i] >= target);
int last = LowerBound(arr.Length, i => arr[i] > target) - 1;
if (first == arr.Length || arr[first] != target) return (-1, -1);
return (first, last);
}Step 4: Binary search on the answer (optimization)
// Capacity-planning: smallest number of trucks that can ship every package
// in <= D days, given truck capacity. Predicate is monotonic in capacity.
public static int MinCapacity(int[] weights, int days)
{
int low = weights.Max(); // can't be smaller than heaviest pkg
int high = weights.Sum() + 1; // upper bound: one truck does it all
bool Feasible(int cap)
{
int trucks = 1, load = 0;
foreach (var w in weights)
{
if (load + w > cap) { trucks++; load = 0; }
load += w;
}
return trucks <= days;
}
// LowerBound on a window [low, high) โ same template, works perfectly
while (low < high)
{
int mid = low + (high - low) / 2;
if (Feasible(mid)) high = mid; else low = mid + 1;
}
return low;
}Step 5: Floating-point binary search
// Stop on absolute precision rather than equality
public static double Sqrt(double x, double eps = 1e-9)
{
double low = 0, high = Math.Max(1, x);
while (high - low > eps)
{
double mid = (low + high) / 2;
if (mid * mid > x) high = mid; else low = mid;
}
return low;
}Step 6: Use the BCL when you can
// .NET BCL โ well-tested, supports IComparer<T>, returns bitwise complement
// of the insertion point on miss
int idx = Array.BinarySearch(arr, target);
if (idx < 0) idx = ~idx; // would-insert-at index
// Spans (zero-allocation)
ReadOnlySpan<int> span = arr;
int idxSpan = MemoryExtensions.BinarySearch(span, target);
// SortedSet<T>.GetViewBetween / SortedDictionary for range queries
var sorted = new SortedSet<int>(arr);
var range = sorted.GetViewBetween(10, 20);Pitfalls
1. (low + high) / 2 overflow. When low and high are large positive ints, their sum overflows to negative and you index out of bounds. Always write low + (high - low) / 2.
2. Mixing < and <= with closed intervals. The closed-interval form ([low, high]) needs while (low <= high) and high = mid - 1. Mixing the half-open template with closed-interval termination causes infinite loops or off-by-ones. Pick one and never mix.
3. low = mid instead of mid + 1. Classic infinite loop: whenhigh - low == 1, mid == low; assigning low = midchanges nothing. Always advance by at least one on the "exclude mid" branch.
4. Non-monotonic predicate. Binary search requires predicateto be false-then-true (or true-then-false) across the range. If the array isn't sorted, or the optimization function isn't monotonic, you get wrong answers without a crash. Assert sortedness in debug builds.
5. Floating-point equality. while (low != high) on doubles loops forever due to rounding. Use a tolerance, or run a fixed number of iterations (100 iterations halves the range by 2^100 โ far below any precision you need).
6. Reinventing it.For sorted arrays, lists, or spans of comparable elements, the BCL'sArray.BinarySearch / List<T>.BinarySearch / MemoryExtensions.BinarySearch are battle-tested. Use them unless you genuinely need a custom predicate.
Practical Takeaways
- Memorize one template: half-open
[low, high), predicate-based, returnslow. - For exact-match search, run lower bound and check
arr[idx] == target. - For first/last occurrence, run lower bound twice with
>= targetand> target. - For optimization ("smallest X that satisfies Y"), define a monotonic
Feasible(x)and reuse the same template. - Always write
low + (high - low) / 2โ overflow is real even in 2026 with 64-bit indices. - Floating-point: use
epsor fixed iteration count; never test equality. - Reach for
Array.BinarySearch/MemoryExtensions.BinarySearchon sorted data before rolling your own.

