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

  1. 1Identify the search space: indices of an array, or a numeric range of answers.
  2. 2Write the predicate and confirm it is monotonic (false...false, true...true).
  3. 3Choose the boundary you want: first true or last false.
  4. 4Use one loop shape consistently and know what lo holds when it ends.
  5. 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

OperationCost
Search in a sorted arrayO(log n) time, O(1) space
Search on the answerO(log R) checks; O(n log R) when each check is linear
Sorting first, then searchingO(n log n) once, O(log n) per query
Linear scan it replacesO(n) per query

30 Binary Search problems

Easiest first. Problems marked Free can be solved without Pro.

  1. 1Locate a Catalog NumberFreeEasy
  2. 2Shelf Slot for a New BookFreeEasy
  3. 3Floor of a Square RootFreeEasy
  4. 4First Failing BuildEasy
  5. 5Tally of a Value in a Sorted LogEasy
  6. 6Next Gate LetterEasy
  7. 7Is It a Square Tiling?Easy
  8. 8Latest Reading Not After a TimeEasy
  9. 9Search a Row-Major GridEasy
  10. 10Bounds of a Repeated ValueMedium
  11. 11Highest Point on the RidgeMedium
  12. 12Find in a Rotated RosterMedium
  13. 13Smallest Reading After RotationMedium
  14. 14Rotated Shelf With RepeatsMedium
  15. 15Smallest Van for the WeekMedium
  16. 16Slowest Safe Scanner SpeedMedium
  17. 17K-th Missing Ticket NumberMedium
  18. 18Find in a Two-Way Sorted GridMedium
  19. 19K-th Smallest in a Sorted GridMedium
  20. 20K Readings Nearest a ValueMedium
  21. 21Earliest Day for BouquetsMedium
  22. 22Unpaired Sock in a Sorted DrawerMedium
  23. 23Spread Out the SensorsMedium
  24. 24Shortest Longest Gap After New StationsMedium
  25. 25K-th Entry of a Times TableHard
  26. 26Best Average of a Long Enough StretchHard
  27. 27Median of Two Sorted FeedsHard
  28. 28K-th Smallest Pair GapHard
  29. 29Lowest Reading in a Rotated Log With RepeatsHard
  30. 30Median of a Grid With Sorted RowsHard

Solve them in the browser, in Python or JavaScript.