Sparround

Binary search and search patterns

Binary search halves the search space at every step: log₂ n steps for n elements. On a billion elements that is 30 steps.

The precondition: the array must be sorted. But there is a more general and more useful phrasing: the search space must have a monotone transition, where past some point the answer flips from "no" to "yes" and never flips back. A sorted array is merely the most familiar instance — and that generalisation is the key to the strongest pattern at the end of this topic.

Sorting for a single search doesn't pay: the sort itself is O(n log n) while a linear scan is O(n). Binary search wins only when the data is already sorted or when you sort once and search many times. Say that reasoning out loud in an interview — many candidates answer "I'd sort and binary search" and make a one-off lookup more expensive.

Binary search looks easy to write, yet by common report most professional programmers cannot get it right on the first attempt. Three traps explain that.

Trap 1 — overflow in `mid`. (lo + hi) / 2 on fixed-width integers (Kotlin/Java Int, C++ int) produces a negative number when lo and hi are large. The correct form is `lo + (hi - lo) / 2`. In JavaScript numbers are 64-bit floats so that overflow doesn't occur in practice — but writing (lo + hi) >> 1 walks into a different trap: bitwise operators coerce operands to 32 bits and break for indices above 2³¹.

Trap 2 — the infinite loop. If you write hi = mid and keep lo at mid (i.e. lo = mid), then on a two-element range mid always equals lo and the loop never ends. The rule: the range must shrink on every iteration — one side has to become mid + 1 or mid - 1.

Trap 3 — off-by-one. hi = n or hi = n - 1? while (lo < hi) or while (lo <= hi)? These decisions depend on each other, and mixing them either skips the last element or runs off the end of the array.

The fix — the loop-invariant recipe. Pick one form and stay loyal to it:

  • Use a half-open range: [lo, hi) — that is lo = 0, hi = n (the length, not the last index)
  • Loop condition while (lo < hi)
  • Invariant: the answer is always inside `[lo, hi)`
  • If mid doesn't qualify, lo = mid + 1; if it does, hi = mid (mid itself is still a candidate)
  • When the loop ends lo == hi, and that is the answer

This template cannot loop forever, because either lo grows or hi shrinks. Better still, it returns the lower bound (the first index satisfying the condition) directly — and every variant below is built on top of it.

VariantThe questionChange to the template
Exact searchDoes the element exist, and at which index?Find the lower bound, then check `a[lo] === target`
First occurrence (lower bound)The first index equal to target?Condition: `a[mid] < target` → `lo = mid + 1`
Last occurrence (upper bound − 1)The last index equal to target?Condition: `a[mid] <= target` → `lo = mid + 1`, then take `lo - 1`
Insertion pointWhere do I insert to keep the order?It is the lower bound itself
CountingHow many times does target occur? How many in a range?`upperBound(x) - lowerBound(x)`
Rotated arraySearch in a rotated sorted arrayEach step, decide which half is sorted; if target lies in it, go there
Search on the answer spaceThe minimum/maximum value satisfying a condition?Search over the range `[min, max]`, not the array; replace the comparison with `feasible(mid)`

Binary search on the answer space is the pattern candidates miss most often, which is exactly why interviewers use it as a differentiator.

The key idea: binary search has nothing to do with arrays. All it needs is a monotone feasible(x) function — one that returns true from some threshold onward:

false, false, false, TRUE, true, true, ...

Given that, you can find the first true in log steps — even when the thing you are looking for exists in no array at all.

How to recognise it: if the problem says "find the minimum X such that...", "the maximum X such that...", "at least how many...", "at most how much..." and testing a candidate X is easy, this is the pattern.

The template:

  • Fix the possible range of the answer: lo = the smallest meaningful answer, hi = an answer guaranteed to work
  • Write feasible(x) — usually a simple linear pass, O(n)
  • Use the ordinary lower-bound template: feasible(mid)hi = mid, otherwise lo = mid + 1
  • Complexity: O(n log(range))

Classic examples: the minimum ship capacity to move packages in D days; the minimum speed for Koko to eat the bananas in H hours; splitting n books among k students so the maximum load is minimal; integer square root; the maximum number of servers within a budget.

The critical check: is feasible genuinely monotone? If a "yes" can be followed by a "no" again, binary search does not apply. Verifying that out loud in the interview — "as capacity grows the days needed can only fall, so it's monotone" — is the strongest part of the answer.

Interview tip: after writing a binary search, always dry-run two tiny cases — a single-element array and a target that isn't present. Those two checks catch nearly every off-by-one and take 30 seconds. Interviewers set binary search precisely to watch boundary behaviour: they don't want "the code works", they want you to walk target = 3 and target = 7 through the array [5]. It also helps to know your language's built-in semantics — Kotlin's binarySearch returns -(insertion point) - 1 when the value is absent, so -result - 1 gives you the insertion point.

📚 Sources and documentation