Binary search (incl. on answer)¶
At a glance
Priority: P0 · Complexity: ⅖ · Est. time: 3 h · Phase: 2 · Prereqs: Two pointers You're done when: you write one boundary-search template that you never get off-by-one wrong, recognise "binary search on the answer" (Koko, ship capacity, split array) within 2 minutes, and can explain the partition idea in Median of Two Sorted Arrays.
Why it matters¶
Binary search is much more than "find x in a sorted array". The powerful form is searching for the boundary of a monotone predicate: the first index where ok(i) becomes true. That covers lower/upper bounds, rotated arrays, peak finding, and "minimise the maximum" optimisation problems where you search over the answer space. In systems it shows up as git bisect, B-tree node search, SSTable index lookups, LSM Bloom-then-bisect, capacity planning ("smallest instance count that meets the SLO"), and binary-searching a config value in load tests.
Core concepts¶
Pattern recognition signals¶
| When you see… | Think… |
|---|---|
| Sorted array/matrix, "find target / insert position / first/last occurrence" | Boundary search (lower_bound) |
| "rotated sorted", "find minimum/peak" | Binary search comparing mid to an endpoint/neighbour |
| "minimum speed/capacity/days such that it's feasible", "minimise the maximum", "maximise the minimum" | Binary search on answer + greedy feasibility check |
| n up to 10^9 / answer range huge, O(log) required | Search over the value range |
| Time-versioned lookups "value at time t" | bisect on per-key timestamp list |
| Two sorted arrays, k-th element / median | Partition binary search |
Template 1: first true (the one template to rule them all)¶
def first_true(lo, hi, ok):
"""Smallest x in [lo, hi] with ok(x) True, assuming ok is F...F T...T.
Returns hi + 1 if none. Half-open search on [lo, hi+1)."""
hi += 1
while lo < hi:
mid = (lo + hi) // 2
if ok(mid):
hi = mid # mid might be the answer
else:
lo = mid + 1 # answer is strictly right of mid
return lo
- Lower bound of target in
a:first_true(0, len(a) - 1, lambda i: a[i] >= target). - Last true of a T…T F…F predicate:
first_true(..., lambda x: not ok(x)) - 1. - Python's
bisect_left/rightare this template, so use them for arrays.
Template 2: binary search on the answer¶
import math
def min_eating_speed(piles, h):
def feasible(k): # monotone: faster speed never hurts
return sum(math.ceil(p / k) for p in piles) <= h
return first_true(1, max(piles), feasible)
Complexity: O(n · log(range)). Steps: (1) define the answer space [lo, hi], (2) prove monotonicity of feasible, (3) write a greedy O(n) check.
Template 3: rotated sorted array¶
def find_min_rotated(a):
lo, hi = 0, len(a) - 1
while lo < hi:
mid = (lo + hi) // 2
if a[mid] > a[hi]:
lo = mid + 1 # min is right of mid
else:
hi = mid # mid could be the min
return a[lo]
def search_rotated(a, t):
lo, hi = 0, len(a) - 1
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] == t: return mid
if a[lo] <= a[mid]: # left half sorted
if a[lo] <= t < a[mid]: hi = mid - 1
else: lo = mid + 1
else: # right half sorted
if a[mid] < t <= a[hi]: lo = mid + 1
else: hi = mid - 1
return -1
Template 4: median of two sorted arrays, O(log min(m, n))¶
def find_median_sorted_arrays(a, b):
if len(a) > len(b):
a, b = b, a
m, n = len(a), len(b)
half = (m + n + 1) // 2
lo, hi = 0, m
while lo <= hi:
i = (lo + hi) // 2 # elements taken from a
j = half - i # elements taken from b
a_left = a[i - 1] if i > 0 else float('-inf')
a_right = a[i] if i < m else float('inf')
b_left = b[j - 1] if j > 0 else float('-inf')
b_right = b[j] if j < n else float('inf')
if a_left <= b_right and b_left <= a_right:
if (m + n) % 2:
return max(a_left, b_left)
return (max(a_left, b_left) + min(a_right, b_right)) / 2
if a_left > b_right:
hi = i - 1
else:
lo = i + 1
Variations & decision flow¶
| Variant | Predicate / comparison | Examples |
|---|---|---|
| Exact match | a[mid] == t |
704 |
| Lower / upper bound | a[mid] >= t / a[mid] > t |
35, 34 |
| Rotated | compare a[mid] with a[hi] (or a[lo]) |
153, 33, 81 |
| Peak | a[mid] < a[mid+1] → go right |
162 |
| On answer, minimise | feasible(x) monotone F→T |
875, 1011, 410 |
| On answer, maximise | monotone T→F, find last true | "aggressive cows", magnetic force (1552) |
| Real-valued | loop 100 iterations or hi - lo > eps |
sqrt with precision |
| Partition | cross-compare boundary elements | 4 |
flowchart TD
A[Problem] --> B{Sorted data or<br/>monotone predicate?}
B -- no --> Z[Not binary search]
B -- yes --> C{Searching positions<br/>or values of answer?}
C -- positions --> D{Rotated / peak?}
D -- yes --> E[Compare mid vs hi / neighbour]
D -- no --> F[bisect_left / first_true]
C -- "answer value" --> G[Define lo..hi, write feasible greedy,<br/>first_true over the range]
Common bugs¶
- Infinite loop:
lo = midwithmid = (lo+hi)//2when hi = lo+1. Usemid = (lo+hi+1)//2for "last true", or stick to the one first_true template. - Mixing closed
[lo, hi]and half-open[lo, hi)conventions in one function. - Wrong answer bounds on the answer search (Koko: lo=1 not 0, which would divide by zero. Shipping: lo = max(weights)).
- Integer overflow in
(lo+hi)/2: not an issue in Python, but mentionlo + (hi-lo)//2for Java/C++. - Rotated with duplicates (81):
a[lo] == a[mid] == a[hi]is ambiguous, so shrink both ends. The worst case is O(n).
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| cp-algorithms: Binary search |
article | Predicate-boundary framing, the cleanest mental model | advanced | free |
| NeetCode roadmap: Binary Search | video | Clear visuals for 4, 153 and Koko | intermediate | freemium |
| Python bisect docs | docs | key= and searching sorted lists recipes |
intermediate | free |
| Tech Interview Handbook: Sorting & searching | article | Corner cases list | intermediate | free |
| labuladong: binary search framework |
article | Left/right boundary templates explained side by side | intermediate | freemium |
| Hello Interview: coding patterns | interactive | Binary search on answer walkthroughs | intermediate | freemium |
Hands-on lab¶
| # | LC | Problem | Difficulty | Set | Key insight |
|---|---|---|---|---|---|
| 1 | 704 | Binary Search | Easy | NC150 | Canonical closed interval lo <= hi. |
| 2 | 35 | Search Insert Position | Easy | NC250+ | lower_bound: first index with a[i] >= target. |
| 3 | 374 | Guess Number Higher or Lower | Easy | NC250+ | Search over an API predicate. |
| 4 | 69 | Sqrt(x) | Easy | NC250+ | Largest m with m*m <= x: binary search on answer. |
| 5 | 74 | Search a 2D Matrix | Medium | NC150 | Treat as flattened array: r, c = divmod(mid, cols). |
| 6 | 875 | Koko Eating Bananas | Medium | NC150 | Search on speed; feasibility is monotonic. |
| 7 | 153 | Find Minimum in Rotated Sorted Array | Medium | NC150 | Compare mid to hi, not lo. |
| 8 | 33 | Search in Rotated Sorted Array | Medium | NC150 | One half is always sorted; check if target lies in it. |
| 9 | 981 | Time Based Key-Value Store | Medium | NC150 | Per-key sorted timestamps; bisect_right - 1. |
| 10 | 34 | Find First and Last Position of Element in Sorted Array | Medium | NC250+ | Two lower_bound calls: target and target+1. |
| 11 | 162 | Find Peak Element | Medium | NC250+ | Walk uphill: if a[mid] < a[mid+1], peak is right. |
| 12 | 1011 | Capacity To Ship Packages Within D Days | Medium | NC250+ | Search capacity in [max(w), sum(w)]. |
| 13 | 81 | Search in Rotated Sorted Array II | Medium | NC250+ | Duplicates: when lomidhi, shrink both ends (worst O(n)). |
| 14 | 410 | Split Array Largest Sum | Hard | NC250+ | Minimise the max: binary search + greedy count of parts. |
| 15 | 4 | Median of Two Sorted Arrays | Hard | NC150 | Binary search the partition of the shorter array. |
Stretch exercise: write a property-based test (hypothesis, or random arrays) that checks your first_true against a linear scan for 10,000 random monotone predicates, including empty ranges.
Questions¶
L1 — Recall¶
Q1. What exactly must be monotone for binary search to work?
Answer
The predicate over the search space must be of the form F…F T…T (or the reverse). The data doesn't need to be sorted. For example, "is a[mid] < a[mid+1]" is monotone enough for peak finding in the sense that you can always discard one half safely.
Q2. Why compare with a[hi] rather than a[lo] in Find Minimum in Rotated Sorted Array?
Answer
a[mid] > a[hi] unambiguously means the rotation point (the min) is right of mid. Comparing with a[lo] fails for an unrotated array: a[mid] >= a[lo] holds but the min is at lo, so you'd need an extra check.
Q3. What is the complexity of binary search on the answer?
Answer
O(C · log R), where R is the size of the answer range and C is the cost of the feasibility check (usually O(n)). For Koko: O(n log max(piles)).
L2 — Apply¶
Q4. Implement Capacity To Ship Packages Within D Days.
Answer
O(n log(sum)).Q5. Implement TimeMap (LC 981).
Answer
from bisect import bisect_right
from collections import defaultdict
class TimeMap:
def __init__(self):
self.ts = defaultdict(list); self.vals = defaultdict(list)
def set(self, key, value, timestamp):
self.ts[key].append(timestamp); self.vals[key].append(value) # timestamps increase
def get(self, key, timestamp):
i = bisect_right(self.ts[key], timestamp) - 1
return self.vals[key][i] if i >= 0 else ""
Q6. Find first and last position of target in one reusable helper.
L3 — Design & trade-offs¶
Q7. Split Array Largest Sum: binary search on answer vs DP. Compare.
Answer
DP: dp[i][k] = min largest sum splitting the first i elements into k parts, O(k·n^2) time. Binary search: answer in [max, sum]. The feasibility greedy counts the parts needed with cap x, which is monotone. O(n log sum). Binary search is dramatically faster and simpler. DP generalises to non-monotone cost functions.
Q8. Searching a sorted array that lives on a remote service (each probe = 50 ms RPC). How does that change your approach?
Answer
The cost model becomes probes, not CPU. Options: fetch pages (B-tree-like: each probe returns a block, so the search is log_B n), interpolation search if the values are uniformly distributed (log log n expected), or parallel k-ary search (probe k points concurrently, shrinking the range by k+1 per round). Cache the upper index levels locally. That is exactly why databases use B+trees with large fan-out instead of binary trees.
Q9. When is interpolation search or exponential search preferable?
Answer
Exponential (galloping) search suits unbounded or very large arrays where the target is near the start. Double the bound, then binary search, for O(log i) where i is the position. It's used in Timsort's galloping mode and in posting-list intersection. Interpolation suits uniformly distributed numeric keys (O(log log n) average) but degrades to O(n) on skewed data. Binary search is the robust default.
L4 — Staff-level ambiguity¶
Q10. 'What's the minimum number of pods to keep p99 < 200 ms at 30k RPS?' Frame this as binary search and discuss the caveats.
Answer
Answer space: pods in [1, N_max]. Feasibility: run a load test at n pods and check p99 < 200 ms. Monotone if latency decreases with pods, which is usually true but breaks with shared bottlenecks (a database, a lock), where adding pods doesn't help or even hurts (contention). Each probe is expensive (a 10-minute test), so use a coarse exponential search, then binary search. Latency is noisy, so require k repeated runs or statistical confidence. Monotonicity can be violated by autoscaling or cache warm-up. The deliverable is a capacity model (Little's law, USL fit) validated by a few probes, not only the search.
Q11. Your service stores time-series versions per key in a sorted list and uses bisect for reads. Writes are now out of order. What breaks and what do you change?
Answer
Appending out-of-order timestamps breaks sortedness, so bisect returns wrong versions silently. Options: insort (O(n) insert, fine for short lists), a sorted structure (SortedList, skip list, B-tree: O(log n)), or buffering with a watermark (accept lateness up to Δ, sort the buffer, then append, as stream processors do). Also define semantics for duplicate timestamps (last-writer-wins by sequence number). Add an invariant check or metric for out-of-order writes. The bigger lesson: document the assumptions a data structure relies on and enforce them at the boundary.
Real-world use cases¶
git bisect: binary search over commits for the first bad one (a monotone predicate).- Databases: B+tree page search and SSTable sparse-index lookup, then a scan.
- Versioned configs/prices: "rate effective at time t" is bisect on effective dates (freight tariffs with validity periods).
- Capacity planning and tuning: binary search on thread-pool size or batch size against a throughput SLO.
Pitfalls & anti-patterns¶
- Memorising three different templates and mixing them up. Use one boundary template.
- Wrong search range on the answer space.
- Non-monotone feasibility (check it with a quick counterexample).
- Forgetting the O(n) worst case with duplicates in rotated arrays.
Checklist¶
- My
first_truetemplate passes randomised tests - I can spot "binary search on the answer" and write the feasibility check
- I can explain the median-of-two-arrays partition invariant
- I answered all L3 questions out loud in < 3 min each