Binary Search
Eliminate half the search space with every comparison. It works on any monotonic search space — not just sorted arrays — turning linear scans into O(log n).
Halve it until it's gone
One comparison at the midpoint tells you which half the answer cannot be in —
so you throw that half away. Sixteen candidates become eight, then four, two, one:
log₂(n) comparisons total.
When to reach for it
- "Search in a sorted array."
- "Find the first / last element satisfying a condition."
- "Minimize the maximum" / "maximize the minimum."
- "What is the minimum value such that some condition holds?"
- The array is sorted, or the answer space is monotonic.
The tricky part
Detail 1Mid calculation
# WRONG (can overflow in languages with fixed-size ints)
mid = (left + right) // 2
# CORRECT
mid = left + (right - left) // 2
Detail 2Loop condition & boundary updates
| Detail | Template 1 (exact match) | Template 2 (boundary / lower bound) |
|---|---|---|
| Loop condition | while left <= right | while left < right |
| When to shrink left | left = mid + 1 | left = mid + 1 |
| When to shrink right | right = mid - 1 | right = mid |
| Termination | left > right (not found) | left == right (answer) |
| Returns | mid (exact), or −1 | left (boundary position) |
right = mid (not mid - 1) because mid
itself could still be the answer. Using mid - 1 might skip the answer.
The three templates to memorize
T1Classic binary search — find exact target
def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid # Found it
elif nums[mid] < target:
left = mid + 1 # Target is in right half
else:
right = mid - 1 # Target is in left half
return -1 # Not found
T2Lower bound — first position ≥ target
def lower_bound(nums, target):
"""Return the first index where nums[index] >= target."""
left, right = 0, len(nums) # Note: right = len(nums), not len-1
while left < right:
mid = left + (right - left) // 2
if nums[mid] >= target: # Condition met -- could be answer
right = mid # Keep searching left for FIRST
else:
left = mid + 1 # Too small, go right
return left # left == right == answer
Upper bound (first position > target): just change >= to >.
T3Binary search on the answer
When the problem asks to optimize a value and you can check feasibility, binary-search the answer space instead of an array.
def binary_search_on_answer(lo, hi, is_feasible):
"""
Find the minimum value in [lo, hi] such that is_feasible(val) is True.
Assumes: if is_feasible(x) is True, then is_feasible(x+1) is also True.
(Monotonic feasibility)
"""
while lo < hi:
mid = lo + (hi - lo) // 2
if is_feasible(mid):
hi = mid # mid works, try smaller
else:
lo = mid + 1 # mid doesn't work, need bigger
return lo
The problems that keep showing up
P1Search in Rotated Sorted Array — LC 33
def search_rotated(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
# Left half is sorted
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1 # Target in sorted left half
else:
left = mid + 1 # Target in right half
# Right half is sorted
else:
if nums[mid] < target <= nums[right]:
left = mid + 1 # Target in sorted right half
else:
right = mid - 1 # Target in left half
return -1
P2Find Minimum in Rotated Sorted Array — LC 153
def find_min(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[right]:
left = mid + 1 # Min is in right half
else:
right = mid # Mid could be the min
return nums[left]
P3Find Peak Element — LC 162
def find_peak(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] < nums[mid + 1]:
left = mid + 1 # Go toward the rise
else:
right = mid # Mid might be peak
return left
P4Koko Eating Bananas — LC 875 (search on answer)
def min_eating_speed(piles, h):
def can_finish(speed):
hours = 0
for p in piles:
hours += (p + speed - 1) // speed # ceil division
return hours <= h
lo, hi = 1, max(piles)
while lo < hi:
mid = lo + (hi - lo) // 2
if can_finish(mid):
hi = mid
else:
lo = mid + 1
return lo
P5Sqrt(x) — LC 69
def my_sqrt(x):
lo, hi = 0, x
while lo <= hi:
mid = lo + (hi - lo) // 2
sq = mid * mid
if sq == x:
return mid
elif sq < x:
lo = mid + 1
else:
hi = mid - 1
return hi # hi is the floor of sqrt(x)
Complexity reference
| Problem | Time | Space |
|---|---|---|
| Classic binary search | O(log n) | O(1) |
| Lower / upper bound | O(log n) | O(1) |
| Search rotated array | O(log n) | O(1) |
| Find min in rotated array | O(log n) | O(1) |
| Find peak element | O(log n) | O(1) |
| Koko eating bananas (n=piles, m=max pile) | O(n log m) | O(1) |
| Sqrt(x) | O(log x) | O(1) |
Where binary search goes wrong
len(nums) - 1 vs len(nums) depends on the template.
Closed-range Template 1 uses len - 1; half-open Template 2 uses len.
right = mid with while left <= right can loop forever.
Always pair while left < right with right = mid.
nums[left] <= nums[mid]
before deciding where the target can live.
(left + right) // 2 overflows in languages with fixed-size ints.
Use left + (right - left) // 2 everywhere, even in Python, as a habit.
Guess a value, ask "is this feasible?", shrink the range
In the hardest binary-search problems there is no sorted array to search. Instead the answer itself is a number in a range, you cannot compute it directly — but given a candidate you can check it cheaply. Line the candidates up smallest to largest and their verdicts are sorted even though nothing else is:
Three questions before you write a line
- What is the answer? A capacity, a speed, a largest-part sum — a single number, not an index.
- What is the feasibility check, and is it monotonic? Write
is_feasible(value)as a plain greedy scan. If a larger value can ever be worse, binary search is off the table. - Which bounds bracket it? Pick a
lothat obviously fails (or is the smallest legal value) and ahithat obviously works. Sloppy bounds are the number-one bug here, not the loop.
T3+The skeleton
def smallest_feasible(low, high, is_feasible):
"""Smallest value in [low, high] where is_feasible(value) is True.
Requires monotonic feasibility: once a value works, every larger
value works too -- F F F F T T T T -- and we want that boundary.
"""
while low < high:
middle = low + (high - low) // 2
if is_feasible(middle):
high = middle # middle might BE the answer, so keep it
else:
low = middle + 1 # middle fails, and so does everything below it
return low # low == high == the boundary
This is Template 2 with is_feasible(middle) in place of
nums[mid] >= target. For a maximize problem ("largest value that still
works") the strip is T T T F F F: use middle = low + (high - low + 1) // 2,
then low = middle on success and high = middle - 1 on failure. The
+ 1 rounds mid upward and is what stops the infinite loop.
P6Capacity To Ship Packages — LC 1011
Packages must ship in their given order. Guess a truck capacity; the greedy packer then tells you how many days that capacity costs. Bigger truck, never more days — the monotonicity we need.
def ship_within_days(weights, days):
def days_needed(capacity):
"""Load packages in order; start a new day when the truck would overflow."""
days_used = 1
current_load = 0
for weight in weights:
if current_load + weight > capacity:
days_used += 1 # this package begins a new day
current_load = 0
current_load += weight
return days_used
# The truck must at least hold the single heaviest package, and it never
# needs to be bigger than "everything in one day".
low = max(weights)
high = sum(weights)
while low < high:
middle = low + (high - low) // 2
if days_needed(middle) <= days:
high = middle # this capacity is enough, try smaller
else:
low = middle + 1 # too small, the packages spill over
return low
low = 1 in LC 1011 is not wrong (the greedy just reports a huge day count),
but starting low = 0 divides by zero in Koko, and starting
high = max(weights) excludes every valid answer. Sanity-check that
is_feasible(high) is True and is_feasible(low - 1) would be False.
O(n log(range)), not
O(log n). That is still fast, but say it out loud in an interview.
Practice ladder
In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.