Binary Search
Halve the search space every step — on arrays or answers.
- Brute force
- O(n) — scan every candidate (or O(n · range) for answer-search)
- With the pattern
- O(log n) probes — O(n log range) when each probe costs O(n)
- Extra space
- O(1)
In plain English
“Guessing a number between 1 and 1,000 where your friend only says "higher" or "lower": you always guess the middle, so ten guesses are enough. The trick works on anything where a yes/no answer flips exactly once, like finding the slowest walking pace that still gets you home before dark.”
Why it's fast
Brute force checks every candidate one by one. If a yes/no question is monotonic — all False then all True — one probe at the middle rules out half the candidates, giving O(log n) probes. The candidates don't have to be array indices: they can be speeds, capacities or days (binary search on the answer).
Spot it when the problem says…
- sorted array / rotated sorted array
- find the first / last position of
- minimum capacity / speed / time such that…
- O(log n) required
- maximize the minimum (or minimize the maximum)
- find a peak / boundary
Visualize
Binary search in a sorted array
Return the index of target in a sorted array, or -1 if it isn't there.
Search space is the whole array: 10 candidates.
1def search(nums, target):2 lo, hi = 0, len(nums) - 13 while lo <= hi:4 mid = (lo + hi) // 25 if nums[mid] == target:6 return mid7 if nums[mid] < target:8 lo = mid + 1 # answer is right of mid9 else:10 hi = mid - 1 # answer is left of mid11 return -1variables
- lo
- 0
- hi
- 9
- target
- 23
How to apply it
- 01Define a predicate
ok(x)that is False…False, True…True over the search range. - 02Set
loandhito bound the answer (inclusive or half-open — pick one and stick to it). - 03Loop:
mid = (lo + hi) // 2; ifok(mid)shrink to the left half (keep mid), else move right past mid. - 04When the loop ends,
lois the first True — check it's in range before returning.
Variations you'll meet
Classic exact match
The visualizer's example: find target in a sorted array, closed interval [lo, hi].
def search(nums: list[int], target: int) -> int: lo, hi = 0, len(nums) - 1 while lo <= hi: mid = (lo + hi) // 2 if nums[mid] == target: return mid if nums[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1First True / lower bound template
The one template to memorize. Half-open [lo, hi); returns the first index where ok is True (or hi if none). Same as bisect_left when ok = nums[i] >= target.
def first_true(lo: int, hi: int, ok) -> int: # smallest x in [lo, hi) with ok(x) True; hi if none while lo < hi: mid = (lo + hi) // 2 if ok(mid): hi = mid # mid might be the answer else: lo = mid + 1 # mid is definitely not return lodef lower_bound(nums: list[int], target: int) -> int: return first_true(0, len(nums), lambda i: nums[i] >= target)Binary search on the answer (Koko)
Search the speed, not the array. ok(speed) = can she finish within h hours? Faster always works if slower works, so it's monotonic.
def min_eating_speed(piles: list[int], h: int) -> int: def ok(speed: int) -> bool: hours = sum((p + speed - 1) // speed for p in piles) return hours <= h lo, hi = 1, max(piles) while lo < hi: mid = (lo + hi) // 2 if ok(mid): hi = mid else: lo = mid + 1 return loRotated sorted array
At least one half around mid is sorted; check whether the target lies in that sorted half to decide which way to go.
Common mistakes
- Mixing templates:
while lo < hiwithhi = mid - 1skips the answer;while lo <= hiwithhi = midloops forever. - Setting the answer range wrong for search-on-answer (e.g.
lo = 0causes division by zero in Koko). - Using a non-monotonic predicate — binary search silently returns garbage.
- Computing hours with float division instead of ceiling integer division
(p + s - 1) // s. - Not checking the returned index is in bounds and actually equals target for lower-bound searches.
Practice ladder
Easy → hard. Tick them off as you go.
Think you've got it?
Let the AI interviewer throw a Binary Search problem at you and grade how you explain it.