Binary Search and Boundary Finding
Search sorted data in logarithmic time, find first and last valid positions, and binary-search an answer when feasibility is monotonic.
The promise that makes halving possible
Binary search needs a monotonic search space: once a condition changes from false to true, it never changes back, or values are sorted so comparison tells which side is impossible.
For exact search in an ascending array, maintain an inclusive range [lo, hi] that may still contain the target:
- Compute
mid = lo + (hi - lo) // 2. - If the middle value is the target, return it.
- If the middle value is too small, discard
lo..mid. - If it is too large, discard
mid..hi.
The range halves each time, so an array of one million values needs about 20 comparisons.
Search for 31 in a sorted array
mid = 4, value = 23
Discard indices 0 through 4.
Loop invariants and templates
A loop invariant says what remains true before every iteration. In exact search, the invariant is: if the target exists, it is inside [lo, hi]. Every update must preserve it.
Two templates are common:
| Goal | Range | Continue while | Typical result |
|---|---|---|---|
| Find an exact value | inclusive [lo, hi] | lo ≤ hi | index or −1 |
| Find first valid position | half-open [lo, hi) | lo < hi | insertion boundary |
For the first position where a predicate is true:
- if
valid(mid)is true, keep mid by settinghi = mid, - otherwise discard mid by setting
lo = mid + 1, - when
lo == hi, that boundary is the answer.
This same lower-bound pattern finds the first value at least a target, the first occurrence among duplicates, and the insertion position that preserves sorted order.
Binary search on the answer
Sometimes the input is not the search space. Suppose a problem asks for the smallest capacity that can ship packages within D days. For any capacity:
- capacities below some boundary are impossible,
- every capacity at or above the boundary is possible.
That feasibility predicate is monotonic, so binary-search capacity. This pattern appears in minimum speed, maximum minimum distance, workload allocation and cutting problems.
The design steps are:
- State exactly what
valid(x)means. - Prove that validity is monotonic.
- Choose bounds that definitely contain the answer.
- Use lower-bound search for the first valid answer or upper-bound search for the last valid answer.
Rotated arrays
In a sorted array rotated once, at least one half around the middle is still sorted. Identify that half, check whether the target lies inside its value range, and keep or discard it accordingly. Duplicates make the decision less informative; when nums[lo] == nums[mid] == nums[hi], shrinking the boundaries may be necessary.
Lower bound implementation
The program returns the first index whose value is at least the target. If every value is smaller, it returns the array length.
def lower_bound(nums, target):
lo, hi = 0, len(nums)
while lo < hi:
mid = lo + (hi - lo) // 2
if nums[mid] >= target:
hi = mid
else:
lo = mid + 1
return lo
nums = [2, 4, 4, 4, 9, 13]
for target in (4, 5, 20):
print(target, "->", lower_bound(nums, target))4 -> 1 5 -> 4 20 -> 6
Failure modes
- Updating
lo = midin a lower-bound loop can stop progress whenmid == lo; discard withmid + 1when mid is known invalid. - Mixing inclusive and half-open templates causes skipped candidates or out-of-range access.
- Binary-searching a non-monotonic predicate can return a plausible but meaningless boundary.
- If arithmetic bounds can be large, compute the middle as
lo + (hi - lo) / 2to avoid overflow in fixed-width languages.
Practice exact search in Binary Search, partitioning in Sort Three Colours, and divide-and-conquer counting in Count Inversions.
Practise what you learnt
Easy · Binary Search
Medium · Dutch National Flag
Hard · Merge Sort
Easy · Boundary Binary Search
Medium · Dual Boundary Search
Medium · Modified Binary Search
Medium · Slope Binary Search
Easy · Answer-Space Binary Search
Medium · Missing-Count Binary Search
Easy · Merge Step