Binary Search & Variants
- Write a bug-free classic binary search and defend every boundary choice
- Explain invariant thinking as the cure for off-by-one errors
- Implement first/last-occurrence (boundary) search
- Recognize and solve "search on the answer space" problems
- Use the bisect module for insertion points and range counting
| Spaced-rep warm-up: due cards (sorting, graphs, heaps) | 10 min |
| ELI5 + tech read; step the visualizer and predict each probe | 18 min |
| Guided: classic + boundaries, rotated + Koko | 42 min |
| Practice: ship packages within D days | 20 min |
| Project: toolkit + config auto-tuner | 20 min |
| Quiz + flashcards | 10 min |
Builds on: Day 22 โ Big O & complexity ยท Day 32 โ Sorting ยท Day 29 โ Binary trees & BSTs
Finding "Nakamura" in a paper phone book, nobody starts at page 1. You split the book in the middle โ "Miller" โ too early, so Nakamura lives strictly in the right half, and you have eliminated half the book with one glance. Split the survivor, glance, discard half again. A 1,000-page book takes ten glances; a million pages, twenty. That is binary search: each look kills half the possibilities, which is Day 22's O(log n) signature.
The famous catch: binary search is easy to describe and notoriously easy to implement wrong โ the first published version took years to get right in print. The cure is not cleverness but a *promise you refuse to break*: "the answer, if it exists, is always inside [lo, hi]." Every line of the loop must keep that promise true โ that is an invariant, and checking your code against it beats staring at ยฑ1s.
The bigger unlock: the "phone book" does not have to be a list. Any question shaped "no no no no YES yes yes" โ falses then trues โ can be halved. "Can I ship all packages with capacity C?" is monotonic in C, so you binary-search over *candidate answers*, using a checker function as your glance.
O(log n) is the difference between touching 20 items and 20 million, which is why binary search underlies everything fast: Day 38's B-tree index descent is binary search adapted for disk pages, git bisect halves your commit history to find the breaking change (a debugging move you will use for real), and capacity tuning ("lowest rate limit that stops 429s", "largest chunk size that stays under the token budget" on Day 114) is search-on-answer thinking. In interviews, boundary variants ("first bad version", "search rotated array", "Koko eating bananas") are prized precisely because they punish sloppy invariants โ the discipline is the skill being tested.
Watch the range halve โ find 23 in 9 sorted values
step 1 / 6A sorted array. Target: 23. Binary search only works because order lets us discard half at every step.
Guided practice
The classic + the boundary template
20 min- Create
binary_search_lab.py. Writebinary_search(a, target)from the invariant, narrating each branch: "mid is too small, so the answer lives in [mid+1, hi]". - Fuzz it โ the starter compares your function against
list.indexon 1,000 random sorted lists including empties, singletons, missing targets, and duplicates. A subtle off-by-one WILL be caught here; fix by re-checking the invariant, not by trial-and-error ยฑ1. - Classic 1 โ first and last occurrence. Implement
first_posandlast_poswith the record-and-continue boundary template. Test on [5, 7, 7, 7, 8] for target 7 โ (1, 3), and on a missing target โ (-1, -1). - Cross-check with
bisect_left/bisect_rightand confirm: count of 7s == bisect_right - bisect_left. Two lines, no loop โ the stdlib version of what you just wrote.
Rotated arrays + Koko eats bananas, think-aloud
22 min- Classic 2 โ minimum of a rotated sorted array ([4,5,6,7,0,1,2]). Think aloud: "Sorted-then-rotated means TWO sorted runs; the minimum is the seam. Compare a[mid] with a[hi]: if a[mid] > a[hi], the seam is strictly right of mid; else it is at mid or left." Implement
find_min_rotatedand test rotations 0..n-1 of range(8) exhaustively in a loop โ the not-rotated-at-all case is the one that catches people. - Classic 3 โ Koko eating bananas (search on answer). Koko has piles of bananas and h hours; eating speed k means each pile takes ceil(pile/k) hours. Find the MINIMUM k that finishes in time. Think aloud: "I cannot binary search the piles โ but feasibility is monotonic in k: too slow fails, fast enough always works. So the answer space [1, max(pile)] is my phone book and feasible() is my glance."
- Implement with the boundary template over the answer space. Test piles=[3,6,7,11], h=8 โ 4.
- State the complexity: O(n log max_pile) โ and generalize aloud: "monotonic feasibility + numeric answer = binary search the answer."
On your own
Ship packages within D days
20 minA conveyor belt must ship packages (given weights, in order โ no reordering) within D days. Each day you load consecutive packages up to a capacity limit. Find the minimum capacity that ships everything in D days.
Constraints: O(n log(sum of weights)). Identify lo (think: what capacity is forced by the heaviest single package?) and hi before coding. Test: weights 1..10, D=5 โ 15.
Hints: this is Koko in a shipping costume โ feasible(capacity) greedily fills days and checks days_used <= D. lo = max(weights) (a package cannot be split), hi = sum(weights) (one giant day).
Binary search toolkit + a config auto-tuner
In dsa/binsearch.py, collect today's five functions (classic, first/last, rotated min, Koko, ship-capacity) with the fuzz tests. Then the transfer task, autotune.py: the starter's simulate(batch_size) function (provided stub โ it mimics a service that gets faster with bigger batches until it starts failing past a hidden memory threshold) must be tuned. Binary-search the largest batch size whose simulate() call reports success, in at most 12 probes for a 1..4096 range. Print each probe โ the log should read like a bisection. Commit; write one docstring line connecting this to git bisect.
Common mistakes & misconceptions
- Fixing off-by-ones by sprinkling ยฑ1 until tests pass. State the invariant ("answer is in [lo, hi]") and derive every update from it โ that is the actual skill.
- Writing lo = mid (not mid + 1) after ruling mid out โ on a two-element range mid == lo and the loop never shrinks: infinite loop.
- Returning ANY match when the problem needs the FIRST. Duplicates demand the boundary template (record and continue), not the classic three-way search.
- Missing that binary search requires sorted data โ or a monotonic predicate. On unsorted data it returns garbage without erroring.
- Not recognizing search-on-answer: if the answer is numeric and feasibility is monotonic ("if k works, k+1 works"), the answer space is the array.
- Confusing bisect_left and bisect_right: left = first insertion point (first occurrence); right = past the last. Their difference counts occurrences.
Q1. Your binary search loops forever on a = [3, 8] searching for 8. The most likely bug?
Q2. Which problem is NOT solvable by binary search?
Q3. In sorted a with duplicates, what does bisect_right(a, x) - bisect_left(a, x) compute?
Go deeper โ curated resources
- git bisect on a real repo โ Intentionally break a test in your practice repo several commits back, then use git bisect run pytest to find the culprit automatically. Binary search over history โ 4 probes for 16 commits.
- Classic search passes the 1,000-case fuzzer untouched
- First/last occurrence matches bisect_left/bisect_right on all tests
- Koko and ship-capacity solved with the search-on-answer template
- Auto-tuner converges in โค 12 probes; toolkit committed
- Quiz โฅ 2/3
โ Back: The halving is Day 22's O(log n) row made mechanical, Day 32's sorting is the precondition that makes it legal, and Day 29's BST search was this same walk through pointers instead of indexes.
Forward โ: Day 38's B-tree index descent is binary search restructured for disk. Day 34 meets problems where halving fails and memoization takes over. The search-on-answer pattern returns for capacity planning on Day 160, and git bisect is this algorithm applied to your commit history.
Unlocks: D35 Week 5 Checkpoint: Interview Drill I ยท D38 Database Internals