Theory · Binary Search
DSA Roadmap Open Visualizer Back to intro

Algocraft · Theory

Search, and the power of a good invariant

"Searching" sounds like it means scanning — checking things one by one until you find what you want. Binary search is the opposite idea: instead of looking at every candidate, you throw away half of the remaining possibilities at every single step, without ever looking at most of them. That trick doesn't just speed up looking things up in a sorted array — the same reasoning applies to a much bigger family of problems, once you learn to see it. That generalization is really the point of this page.

01 · Foundation

Why binary search works

Linear search checks elements one at a time: look at index 0, then 1, then 2, and so on until you either find the target or run out of array. That's O(n) — in the worst case you touch every element once.

Binary search does something fundamentally different. At each step it picks a single point, asks one yes/no question about it, and uses the answer to discard half of everything that's left — without ever inspecting most of the discarded half. What makes that safe is a property called monotonicity: as you move across the search space, some predicate about each position is false for a while and then becomes true (or the values are simply ordered, which is the special case "sorted" gives you). Once you know the predicate flips at most once across the whole range, checking the midpoint tells you unambiguously which side the answer is on — you never have to check the other side at all.

That's the real requirement underneath "the array must be sorted." Sortedness is just the most common way a monotonic predicate shows up: "is array[i] ≥ target?" is false for small i and true for large i, with a clean flip point. Whenever you can state a similar false-then-true (or true-then-false) predicate over some ordered space, binary search applies — sorted array or not.

Iteration 1 · lo=0 hi=14 mid=7 → arr[7]=27, 27 > 23, search left 27 lo=0 hi=14 Iteration 2 · lo=0 hi=6 mid=3 → arr[3]=12, 12 < 23, search right 12 lo=0 hi=6 Iteration 3 · lo=4 hi=6 mid=5 → arr[5]=19, 19 < 23, search right 19 lo=4 hi=6 next: lo=hi=6 → arr[6]=23, found

Searching a 15-element sorted array for target 23. Each row is one iteration: the cyan bar is what's still in play, gray is what's already been ruled out for good, and the amber marker is the mid just checked. Three comparisons and the live range has gone from 15 elements to 1 — that's the shape of O(log n).

The invariant that makes this correct: at every point in the loop, if the target exists, it is guaranteed to be inside [lo, hi]. Each iteration must preserve that guarantee while shrinking the range — that single sentence is the entire correctness argument for every binary search variant you'll ever write.

02 · Method

A repeatable framework

Binary search is short to code and surprisingly easy to get subtly wrong. Working through these five decisions, in order, avoids almost every bug people run into.

  1. 1 Confirm a monotonic predicate actually existsBefore reaching for binary search, check that your search space really is false-then-true (or ordered) across the range you care about. Forcing it onto unsorted or non-monotonic data doesn't just run slowly — it gives wrong answers.
  2. 2 Define lo/hi preciselyDecide up front whether hi is inclusive (the last valid index) or exclusive (one past the last valid index), and stay consistent for the rest of the function. Mixing conventions mid-way is the single biggest source of off-by-one bugs.
  3. 3 Compute mid and evaluate the predicateCompute a midpoint between lo and hi, and ask your yes/no question about it — usually "is array[mid] equal to, less than, or greater than the target?"
  4. 4 Decide which half to keepBased on the predicate's result, update lo or hi to discard the half that can't contain the answer. This is where off-by-one bugs actually live — make sure the half you discard includes mid itself when it's been ruled out, and the half you keep never accidentally re-includes it.
  5. 5 Define the stopping conditionMatch the loop test (lo <= hi vs lo < hi) to the inclusive/exclusive convention you picked in step 2, and decide explicitly what the function returns when the target simply isn't there.

03 · Worked example

Binary search, one comparison at a time

Array [2, 5, 8, 12, 16, 23, 38, 45, 56, 72] (10 elements, indices 0–9), inclusive bounds, target 23. Each row is one loop iteration: compute mid, read array[mid], decide.

iterationlomidhiarray[mid]decision
1 04916 16 < 23 → go right, lo = 5
2 57945 45 > 23 → go left, hi = 6
3 55623 23 = 23 → found at index 5

Three iterations, each one a single comparison, and the range went from 10 candidates to exactly 1. Notice lo and hi only ever move inward — that's the invariant from the callout above being maintained by hand, one step at a time.

04 · Complexity

How to reason about time & space

Every iteration removes half of whatever's left, so the number of iterations to get down to a single element is the number of times you can halve n before hitting 1 — that's log₂ n by definition. Time complexity: O(log n).

05 · Generalization

Beyond array lookup — binary search on the answer

Here's the part that matters more than the array version: binary search doesn't need a literal sorted array to work. All it needs is a range of possible answers and a feasibility check — some function that takes a candidate answer and returns "yes, this works" or "no, this doesn't" — where the check is monotonic across the range (once it flips from no to yes, it stays yes for every larger, or every smaller, candidate).

When that holds, you can binary search directly over the space of possible answers, even though nothing is stored or sorted anywhere. Pick a candidate in the middle of your answer range, run the feasibility check, and use the yes/no result to discard half of the remaining candidates — exactly the same loop as the array version, just with "is arr[mid] big enough?" replaced by "does this candidate answer actually work?"

A few classic phrasings that are quietly asking you to do this:

The mental shift is: stop asking "is my data sorted?" and start asking "if I fixed a candidate answer, could I cheaply check whether it works — and would that check's answer flip at most once as I sweep the candidate up or down?" If yes, you have a binary search, regardless of what the input looks like.

06 · Before you start coding

Common mistakes

07 · Try it

See it animate

One visualizer currently covers this category directly — but everything above applies well past this one use case.