Pattern 05 of 20
Binary Search
Halve the search space each step, over an array or over the range of possible answers.
When to use it
- The input is sorted or rotated-sorted
- You can test a candidate answer with a yes/no check that is monotonic
- Constraints are large enough that O(n) is too slow
The idea
Keep a range that is guaranteed to contain the answer and cut it in half with every comparison. The same idea works on answers: if a value works, every larger value works too, so you search for the boundary.
Typical cost O(log n) per search; O(n log R) when each check is linear.
How it works
Binary search keeps a range that is guaranteed to contain the answer and halves it with each test. Twenty steps cover a million candidates, thirty cover a billion, so it is the default whenever the input is sorted or the constraints rule out a linear scan.
The robust mental model is a boundary, not a lookup. Imagine a predicate that is false for every candidate up to some point and true from then on. Binary search finds the first true. Searching for a value, an insertion point, or the first bad version are all the same boundary search.
The same idea works on answers. If you can check 'is capacity c enough?' in linear time, and enough-ness only improves as c grows, then you binary search c across its possible range. This is how shipping, eating-speed and split-array problems are solved.
Look for sorted or rotated input, a phrase like 'minimum value such that', or limits up to 10^9 where only a logarithmic number of checks is affordable.
How to attack it
- 1Identify the search space: indices of an array, or a numeric range of answers.
- 2Write the predicate and confirm it is monotonic (false...false, true...true).
- 3Choose the boundary you want: first true or last false.
- 4Use one loop shape consistently and know what lo holds when it ends.
- 5Test with one and two elements, and with the answer at either end.
Template
A skeleton to adapt, not a solution to paste. Write it from memory a few times.
def first_true(lo, hi, ok):
# Smallest x in [lo, hi] with ok(x) True; returns hi + 1 if none.
# Requires ok to be monotonic: False ... False True ... True.
while lo <= hi:
mid = lo + (hi - lo) // 2
if ok(mid):
hi = mid - 1 # mid works; look for an earlier one
else:
lo = mid + 1 # mid fails; the answer is to the right
return lo
def lower_bound(nums, target):
# First index whose value is >= target (insertion point).
return first_true(0, len(nums) - 1, lambda i: nums[i] >= target)
def min_capacity(weights, days):
# Binary search on the answer: the smallest capacity that fits in 'days'.
def fits(cap):
used, load = 1, 0
for w in weights:
if load + w > cap:
used, load = used + 1, 0
load += w
return used <= days
return first_true(max(weights), sum(weights), fits)Variants
- Exact match
- Return mid when nums[mid] equals the target; otherwise discard the half that cannot contain it.
- Lower and upper bound
- First index with value >= target and first with value > target; their difference counts occurrences.
- Rotated sorted array
- At least one half around mid is sorted; check whether the target lies inside that half to decide where to go.
- Search on the answer
- Binary search a numeric range with a feasibility check, for 'minimum maximum' and 'smallest speed that works' problems.
- Real-valued search
- For square roots or continuous answers, loop a fixed number of times (say 100) instead of comparing indices.
Pitfalls
- Mixing loop shapes (lo < hi with hi = mid - 1), which skips answers or loops forever.
- Choosing a predicate that is not monotonic, so halving discards the answer.
- Setting the answer range too tight, for example a lower bound of 0 when the answer is at least max(weights).
- Integer overflow with (lo + hi) / 2 in fixed-width languages; lo + (hi - lo) / 2 is the safe habit.
- Forgetting that the result may be out of range (no element satisfies the predicate).
What it costs
| Operation | Cost |
|---|---|
| Search in a sorted array | O(log n) time, O(1) space |
| Search on the answer | O(log R) checks; O(n log R) when each check is linear |
| Sorting first, then searching | O(n log n) once, O(log n) per query |
| Linear scan it replaces | O(n) per query |
30 Binary Search problems
Easiest first. Problems marked Free can be solved without Pro.
- 1Locate a Catalog NumberFreeEasy
- 2Shelf Slot for a New BookFreeEasy
- 3Floor of a Square RootFreeEasy
- 4First Failing BuildEasy
- 5Tally of a Value in a Sorted LogEasy
- 6Next Gate LetterEasy
- 7Is It a Square Tiling?Easy
- 8Latest Reading Not After a TimeEasy
- 9Search a Row-Major GridEasy
- 10Bounds of a Repeated ValueMedium
- 11Highest Point on the RidgeMedium
- 12Find in a Rotated RosterMedium
- 13Smallest Reading After RotationMedium
- 14Rotated Shelf With RepeatsMedium
- 15Smallest Van for the WeekMedium
- 16Slowest Safe Scanner SpeedMedium
- 17K-th Missing Ticket NumberMedium
- 18Find in a Two-Way Sorted GridMedium
- 19K-th Smallest in a Sorted GridMedium
- 20K Readings Nearest a ValueMedium
- 21Earliest Day for BouquetsMedium
- 22Unpaired Sock in a Sorted DrawerMedium
- 23Spread Out the SensorsMedium
- 24Shortest Longest Gap After New StationsMedium
- 25K-th Entry of a Times TableHard
- 26Best Average of a Long Enough StretchHard
- 27Median of Two Sorted FeedsHard
- 28K-th Smallest Pair GapHard
- 29Lowest Reading in a Rotated Log With RepeatsHard
- 30Median of a Grid With Sorted RowsHard
Solve them in the browser, in Python or JavaScript.