{} Coding Interviews · ch.09 · binary search
🧩 Part II · Linear Structures · chapter 9 / 24

Halve the world until
only the answer remains

Binary search isn't an array trick — it's what you do to anything monotonic: a sorted array, a rotated one, or the space of possible answers itself. One six-line loop, learned once with its off-by-ones filed down, finds any yes/no boundary in O(log n).

Chapter 3 told you that n ≈ 10⁹ in the constraints leaves you two survivors: O(log n) and O(1). This chapter is the first survivor. Binary search is the only pattern in the book that routinely beats a billion elements before your coffee cools — thirty probes, done.

But the version most people carry around — "look for a value in a sorted array" — is the smallest room in the house. The full pattern is a boundary hunt on anything monotonic, and its best trick is searching arrays that were never built. We'll take it in three sittings: the discipline (so the off-by-ones stop biting), the rotated-array disguise, and search-on-answer — the superpower that comes back in Chapter 17 wearing a wetsuit.

1Twenty questions, one billion numbers — the halving superpower

You already play binary search at parties. "I'm thinking of a number from 1 to 100" — nobody guesses 1, then 2, then 3. You guess 50, hear "higher", and just deleted half the universe with one question. Seven questions finish 100 numbers; twenty finish a million; thirty finish a billion. That's O(log n) — halve the candidates per step, and the step count is "how many times can I halve n", which is log₂ n.

The party trick works because the oracle's answers are consistent with an ordering: "higher" doesn't just kill 50, it kills everything below 50. That's the real requirement — not "the input is a sorted array" but "one probe's answer tells me about half the candidates at once". Sorted arrays merely happen to be the most common place this holds. Keep that distinction warm; Section 5 cashes it in.

One more framing before the machinery. Every log you'll ever quote in an interview deserves a gloss, so here's this one: O(log n)double the input, pay one more step. It's the complexity class that makes "the array has 10⁹ elements" a threat in every pattern except this one.

🎯
The tell: the input is sorted — or the constraints scream n ≥ 10⁸, or the problem demands O(log n) outright. Any of the three should make your hand reach for lo/hi/mid before your brain finishes reading. (Sorted also invites Chapter 5's two pointers — pointers when you want a pair, halving when you want a position.)

2Every search is a boundary hunt — first-true thinking

Here is the single reframe that retires 90% of binary-search bugs. Forget "find the value". Instead, picture a row of answers to some yes/no question, and notice that monotonicity forces them into two clean runs:

✗ ✗ ✗ ✗ ✗ ✗ ✓ ✓ ✓ ✓

All the noes, then all the yeses. Somewhere between them sits the boundary — the first ✓ — and that is what binary search finds. Every classic is this picture with a different question:

  • Binary Search Easy — question: "is a[i] ≥ target?" The first ✓ is where the target lives (or would live).
  • First bad version — question: "is version i bad?" The first ✓ is the culprit commit. (This is also literally git bisect.)
  • Koko Medium — question: "can she finish at speed k?" The first ✓ is the minimum viable speed. No array anywhere.

Why does this framing kill bugs? Because "find the value" has three outcomes to juggle (found it, go left, go right), while "find the first ✓" has exactly two: mid is a ✓ (the boundary is here or to the left) or mid is a ✗ (the boundary is strictly to the right). Two outcomes, two updates, no special cases. The loop practically writes itself — next section, it does.

💡
Monotonicity is the license. Before you binary-search anything, say one sentence out loud: "if the answer is yes at k, it's yes for everything past k." If you can't say it, you don't have a boundary — you have a haystack, and you need a different chapter.

3lo, hi, mid — six lines and their discipline

Here's the loop, in the one shape this book will use everywhere. lo and hi bracket the candidates; the invariant — the sentence that stays true every lap — is "the boundary is somewhere in [lo, hi]":

while lo < hi: mid = (lo + hi) // 2; if check(mid): hi = mid; else: lo = mid + 1

Read the asymmetry — it's the whole trick. A ✓ at mid means mid might itself be the first ✓, so it must stay in the range: hi = mid, not mid − 1. A ✗ at mid means mid is definitely dead, so we may skip past it: lo = mid + 1. One side keeps mid, the other side buries it. The loop ends when lo == hi — a range of one candidate, which by the invariant must be the boundary.

And the classic terror — "does it loop forever?" — has a one-line audit: with a floor mid, mid < hi whenever the range has ≥ 2 elements, so hi = mid strictly shrinks the range, and lo = mid + 1 obviously does. Floor-mid pairs with hi = mid; that pairing is the immunization. Memorize the pair, not twelve variants.

Now go play the game below. You probe, the range shrinks, and the counter holds you to the mathematically fair budget of ⌈log₂ n⌉ probes. The part worth feeling in your hands: after each probe you choose which half survives — and the widget calls it out when you bury a candidate that might have been the answer.

Interactive · the bounds game Find the hidden first ✓ · probe mid, then pick the surviving half
n = 64
candidates left
64
probes used
0 / 6
wrong halves picked
0
status
hunting…
⚠️
The three classic wounds, so you can dodge them by name: (1) hi = mid − 1 after a ✓ — you just buried a candidate that might be the answer; (2) lo = mid after a ✗ with floor-mid — a two-element range stops shrinking and the loop spins forever; (3) in fixed-width languages, mid = (lo + hi) / 2 can overflow — write lo + (hi − lo) / 2. Python shrugs at (3); your Scala interviewer won't.

4A sorted array with a scar — rotated arrays

Interviewers love breaking sortedness just a little. Take a sorted array, chop it once, swap the pieces: [4,5,6,7,0,1,2]. Search in Rotated Sorted Array Medium asks for a target in O(log n); Find Minimum in Rotated Sorted Array Medium asks where the scar is. A linear scan is off the table — the whole point is to halve anyway.

The saving grace, and the only new idea you need: cut a rotated array anywhere, and at least one of the two pieces is perfectly sorted. The scar can't be in both halves — there's only one scar. So each lap:

  • Compare a[lo] ≤ a[mid]. If yes, the left half is the clean one; otherwise the right half is.
  • Interrogate the clean half — it's sorted, so "is the target inside you?" is one range check: a[lo] ≤ t < a[mid].
  • If the target's in the clean half, keep it; if not, it must be hiding in the scarred half. Either way: half the array gone, same as always.

Find Minimum is the same insight aimed at the scar itself: the minimum is the one place where sortedness breaks, and "is a[mid] ≤ a[hi]?" is a monotonic question about which side of the scar you're on — first-true thinking again, no target required.

Interactive · rotated-array explorer Rotate the array, pick a target, watch which half stays sorted
pick a target, then Search
5
probes
0
result
worst case (n = 16)
5 probes
🎯
The tell: "sorted array, then rotated / shifted at an unknown pivot" — or any almost-sorted input with an O(log n) demand attached. The move is always the same sentence: "one half is still sorted; I'll ask that half about the target." Say it before you code and the interviewer relaxes visibly.

5The array that isn't there — binary-searching the answer

Now the superpower. Koko Eating Bananas Medium: piles of bananas [3, 6, 7, 11], guards back in h hours, Koko eats at speed k bananas/hour (one pile at a time, a pile-hour is spent even if the pile runs out mid-hour). Find the minimum k that finishes in time. There is no array to search. Nothing is sorted. And yet this is the purest binary search in the book.

The move: search the space of candidate answers. Speeds run from 1 to max(pile) — faster than the biggest pile buys nothing. For any speed, the question "can Koko finish in h hours at speed k?" has a yes/no answer, and it's monotonic in k: eating faster never makes you slower. So the answer space looks like ✗ ✗ ✗ ✗ ✓ ✓ ✓ — infeasible speeds, then feasible ones — and "minimum feasible speed" is exactly the first-✓ boundary from Section 2. Same six lines; the only new work is writing check(k), here a one-liner: sum(ceil(p / k)) ≤ h.

Cost: O(n log m)each probe walks the n piles once, and there are log₂(max pile) probes. The widget below draws the answer space Koko can't see: drag h to move the feasibility boundary, then watch the search hop straight to it.

Interactive · answer-space search Koko's speeds 1..11 · green = finishes in h hours · search finds the first green
piles [3, 6, 7, 11] · adjust h, then run
h = 8
probes
0
minimum speed
hours at that speed
speeds never probed
🎯
The tell: "minimize the maximum…", "maximize the minimum…", "smallest capacity / speed / days such that it's possible", "can you do it with k?" — none of these mention sorting, and all of them are binary-search-the-answer. The deep signature is a monotonic feasibility question: if k works, every friendlier k works too. Spot that sentence and the problem is already half solved.

6The check does the real work — designing check(k)

Once you've decided to search the answer space, the six-line loop is boilerplate; all the actual thinking moves into check(k). Three design notes that cover nearly every problem in this family:

  • The check is usually a greedy simulation. "Can we ship all packages in d days with capacity c?" — walk the packages left to right, stuff each day greedily, count the days. Cheap, linear, and provably enough because feasibility only asks whether it can be done, not how elegantly. This greedy-inside-binary-search sandwich returns in Chapter 19's territory, and Chapter 17's Swim in Rising Water is the graph edition: check(t) = "with water level t, does a path exist?" — a flood fill from Chapter 15 as the feasibility probe.
  • Get the direction straight before coding. Bigger speed → fewer hours (feasibility gets easier); bigger capacity → fewer days. Say which direction ✓ lies in, then decide whether you want the first ✓ (minimizing) or the last ✓ (maximizing). For "last ✓", the cleanest move is to search for the first ✗ and step back one — same loop, no new variant to memorize.
  • Bound the space honestly. lo = the smallest answer that could conceivably work (max single package, for capacity), hi = a trivially feasible answer (sum of all packages). Sloppy bounds don't break correctness, but tight ones make your check's edge cases (k = 0, division by zero) impossible by construction.

Costs compose politely: O(check) × O(log |answer space|). Even a millions-wide answer range contributes a factor of ~20 — the log is doing all the heavy lifting, which is why this pattern turns "optimize a continuous-feeling quantity" problems into linear scans with a hat on.

💡
Interviewers often escalate a solved problem with "now what if the array had 10¹⁸ elements / the capacity could be huge?" That's not a new problem — it's an invitation to point at the log: the search cost grows by the log of the range, so doubling the range costs one probe. Deliver that sentence and the follow-up is over.

7The family album — canonical problems

The problems interviewers actually reach for, and which room of the house each one lives in:

  • Binary Search Easy — the loop, naked. Worth one careful hand-written rep purely to burn in the lo/hi/mid discipline.
  • Search in Rotated Sorted Array Medium & Find Minimum in Rotated Sorted Array Medium — Section 4's scar logic; the second is the first without a target.
  • Koko Eating Bananas Medium — the canonical answer-space search, solved in full below.
  • Time-Based Key-Value Store Medium — store (timestamp, value) pairs per key; get(key, t) wants the latest timestamp ≤ t. Timestamps arrive in increasing order, so each key's list is born sorted — the problem gifts you the sorted array and asks for a boundary ("last ✓ where ts ≤ t"). Design-flavored wrapping, ninety percent binary search.
  • Median of Two Sorted Arrays Hard — the famous one. The move: binary-search how many elements the first array contributes to the left half of the merged order; the check is "is this partition valid?" (every left element ≤ every right element). It's an answer-space search where the answer is a cut position — brutal to derive live, famous enough to rehearse once before an interview loop.

And a practical footnote: real codebases call a library — Python's bisect_left is precisely Section 2's first-✓ search. In interviews, mention it to show you know it exists, then write the loop anyway; the loop is what's being graded. (In Chapter 12 you'll meet the same halving idea frozen into a data structure — the BST.)

8Write it the same way every time — the skeleton

The deepest advice this chapter has: stop improvising binary searches. There are half a dozen popular variants (closed ranges, half-open ranges, lo ≤ hi loops, ±1 on both updates) and they're all correct in someone's hands — but mixing them mid-problem is where the infinite loops breed. Pick the first-true shape, drill it until your fingers own it, and reduce every new problem to "what's my candidate range, what's my check". Under interview adrenaline you don't rise to the occasion; you sink to your most-rehearsed loop. Make it this one.

The pattern, as a whiteboard skeleton:

  1. 1Name the candidate range. Array indices, versions, speeds, capacities — lo = smallest possible answer, hi = a guaranteed-feasible one.
  2. 2Write check(mid) as a monotonic yes/no and say the license out loud: "yes at k ⇒ yes past k". No monotonicity, no binary search.
  3. 3Loop while lo < hi — the range holds the boundary; a range of one IS the boundary.
  4. 4mid = lo + (hi − lo) // 2 — floor mid, overflow-proof form; floor pairs with step 5.
  5. 5✓ at mid → hi = mid — mid may be the first ✓; it must survive.
  6. 6✗ at mid → lo = mid + 1 — mid is dead; bury it and everything before it.
  7. 7Return lo — and if "no answer" is possible, verify check(lo) once before celebrating.
  8. 8Quote the cost with its gloss: O(check × log range) — "each probe halves the range; the check prices the probe".

9Koko, end to end — searching a space no one built

The flagship, both languages. Watch the anatomy match the skeleton line for line: the candidate range is speeds 1..max(piles), the check is an integer-only hours(k) ≤ h (the (p + k − 1) // k idiom is ceiling division without floats — say that out loud, it's a fluency point), and the loop is the first-✓ shape from Section 3, unchanged. The Scala version makes the loop a tail-recursive shrink — same invariant, the recursion is the while.

⚠️
The trap in the check, not the loop: most wrong Koko submissions have a perfect binary search and a broken hours() — float ceils that round wrong on big piles, or forgetting that a 3-banana pile at speed 10 still costs a full hour. When a binary-search solution fails, debug the check first; the six lines almost never lie.