Binary Search & the Answer Space
Halve the search space every step. Then learn to binary-search on answers, not just arrays.
Binary search is the purest example of the “halve the problem” idea. Given a sorted array, you find a value by repeatedly checking the middle and throwing away the half that cannot contain your target. Each comparison cuts the search space in two, so searching a million elements takes about 20 steps, not a million.
Step through it and watch the shaded range collapse. At every step you look at the midpoint and make a decision: if the midpoint is too small, the answer must be to its right, so discard everything up to and including mid. If it’s too big, discard everything from mid rightward. You never look at the discarded half again — that’s where the log n comes from.
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target:
return mid
if arr[mid] < target:
lo = mid + 1 # answer is in the right half
else:
hi = mid - 1 # answer is in the left half
return -1 # not found
The details that trip people up
Binary search is famous for being easy to describe and hard to write correctly. Three things cause almost every bug:
- Loop condition.
lo <= hi(not<) so that a single-element range is still examined. - Midpoint. In languages with fixed-width integers,
(lo + hi) / 2can overflow;lo + (hi - lo) / 2is the safe form. (Python’s big ints make this moot, but interviewers still look for it.) - Shrinking correctly. Always move past
mid(mid + 1/mid - 1) so the range strictly shrinks. Forgetting this is the classic infinite loop.
Binary search on the answer, not the array
The deeper, senior-level insight: binary search doesn’t need an array at all. It needs a monotonic predicate — a yes/no question whose answer flips exactly once as you increase some value. Then you can binary-search for that flip point.
“What is the smallest ship capacity that lets me deliver all packages within D days?” As capacity increases, “can I finish in time?” goes from no to yes and never back. That monotonicity means you can binary-search the capacity — even though there’s no sorted array in sight.
This reframing — binary search on the answer space — turns a whole class of optimization problems (“minimize the maximum,” “find the smallest feasible X”) into O(n log range) solutions. Spotting the hidden monotonic predicate is the skill that separates a rote binary search from a genuinely strong answer.
When to reach for it
Reach for binary search when the data is sorted, or when you can phrase the problem as “find the boundary where a condition flips.” If you can define a feasible(x) check that is monotonic, you can almost always binary-search x.
Pattern recognition
Variations
Worked problems
The code changes less than the question. Spend your planning time defining the predicate and the boundary you want.
Binary Search
Given a sorted array of distinct integers and a target, return the target’s index or -1 if it is absent.
Approach. Keep an inclusive search window [lo, hi]. Compare the middle to the target and discard the half that cannot contain it. Move past mid so the window strictly shrinks.
Show solution
def search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1Complexity. O(log n) time and O(1) space. Each comparison removes half of the remaining window.
Find First and Last Position
Given a sorted array that may contain duplicates, return the first and last index where target appears. If it does not appear, return [-1, -1].
Approach. Write one helper: lower bound, the first index whose value is at least x. The first target is lower_bound(target). The first value greater than target is lower_bound(target + 1), so the last target is one position before that.
Show solution
def search_range(nums, target):
def lower_bound(x):
lo, hi = 0, len(nums)
while lo < hi:
mid = lo + (hi - lo) // 2
if nums[mid] < x:
lo = mid + 1
else:
hi = mid
return lo
left = lower_bound(target)
if left == len(nums) or nums[left] != target:
return [-1, -1]
right = lower_bound(target + 1) - 1
return [left, right]Complexity. O(log n) time and O(1) space. Two boundary searches replace a linear expansion from a found index.
Koko Eating Bananas
Given pile sizes and a deadline in hours, find the smallest integer eating speed that lets all piles be finished in time. In one hour, only one pile can be eaten from, and partial piles round up to a full hour.
Approach. Speed is the answer space. If speed k works, any faster speed also works, so can_finish(k) is monotonic. Search the smallest working speed between 1 and the largest pile.
Show solution
def min_eating_speed(piles, h):
def can_finish(speed):
hours = 0
for pile in piles:
hours += (pile + speed - 1) // speed
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 loComplexity. O(n log m) time and O(1) space, where m is the largest pile. Each feasibility check scans the piles once.