Binary Search Patterns: 3 Templates and Bisect-on-Answer
Master all three binary search templates — exact match, predicate search, and bisect-on-answer. Covers rotated arrays, peak finding, and the hardest LC variant: median of two sorted arrays in O(log min(m,n)).
Why Binary Search Trips Up Strong Candidates
Binary search is the most misimplemented algorithm in FAANG interviews. Candidates who can recite the idea — 'eliminate half the search space each iteration' — still fumble LC 33, 34, or 162 because they're improvising the loop condition and boundary updates instead of applying a fixed template.
The core principle: binary search works whenever you have a monotonic property — a boolean predicate that is False for a prefix of the search space and True for the suffix (or vice versa). You don't need a sorted array. You need a sorted predicate. This reframing unlocks a huge class of problems where binary search is non-obvious: 'minimum capacity to ship packages', 'find peak element in unsorted array', 'median of two sorted arrays'.
There are exactly three templates you need. Pick the right one based on what you're searching for, not by feel. Mixing template idioms is where off-by-one bugs live. The rest of this module walks through each template with canonical problems, then covers the most powerful generalization: bisecting on the answer space.
What Interviewers Are Testing
Interviewers aren't testing whether you know binary search exists. They're testing: (1) Can you identify that binary search applies to a non-obvious problem? (2) Do you get the loop condition and boundary updates right on the first attempt? (3) Can you extend it to bisect-on-answer? A candidate who says 'I'll binary search on the capacity' for a shipping problem — before any coding — signals strong problem-solving instincts. That single insight is worth more than flawlessly coding Template 1.
Choosing the Right Template
Template 1 — Exact target exists
Use when you KNOW the target is in the array and you want its index. Loop condition: left <= right. Update: left = mid + 1 or right = mid - 1. Never exits with left == right without checking mid. Use for: LC 704 (basic search), finding any occurrence of a target.
Template 2 — First index satisfying a predicate
Use when searching for the leftmost position where a condition becomes true. Loop condition: left < right. Update: right = mid (never skip mid, it might be the answer) or left = mid + 1. After loop, left == right is your answer — no post-loop check needed. This is the most general template for LC-style problems.
Template 3 — Peak/boundary needing mid-1 and mid+1
Use only when you need to compare mid with its neighbors AND you need to guarantee neither mid-1 nor mid+1 goes out of bounds. Loop condition: left + 1 < right. After loop, manually check left, mid, right. Rarely needed — prefer Template 2 when possible.
Binary Search Template Decision Flow
Template 1: Exact Target Search
def search(nums: list[int], target: int) -> int:
"""
Template 1: find exact target in sorted array.
Loop invariant: target is in nums[left..right] if it exists.
Terminates when left > right (target not found).
Time: O(log n), Space: O(1)
"""
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2 # avoids overflow in Java/C++
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1 # target not found
Template 2: First Index Where Predicate Is True
def find_first_true(nums: list[int], predicate) -> int:
"""
Template 2: find leftmost index where predicate(nums[i]) is True.
Assumes predicate is False for a prefix, True for a suffix (monotonic).
Loop invariant:
- predicate is False for everything BEFORE left
- predicate is True for everything FROM right onward (once set)
After loop: left == right == answer.
Time: O(log n), Space: O(1)
"""
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if predicate(nums[mid]):
right = mid # mid might be the answer; don't skip it
else:
left = mid + 1 # mid is False, safe to skip
# Post-loop: left == right. Check if predicate holds (it might not if
# no element satisfies the predicate).
return left if predicate(nums[left]) else -1
# Example: find leftmost position to insert target (bisect_left equivalent)
def bisect_left(nums: list[int], target: int) -> int:
left, right = 0, len(nums) # right = len(nums) allows insert-at-end
while left < right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid
else:
left = mid + 1
return left
Bisect-on-Answer: The Most Powerful Generalization
Most candidates know binary search on arrays. The insight that separates strong candidates: you can binary search on any monotonic answer space, even when there's no array at all.
The pattern: some problems ask 'what is the minimum X such that condition C(X) holds?' If C(X) is monotonic — once it's true for some X, it's true for all larger X — you can binary search on X directly.
Identification checklist:
- The question asks for a minimum or maximum value
- You can write a feasibility function
can_do(X)that checks whether X works in O(n) or O(n log n) - If X works, then X+1 also works (or X-1 works — depends on direction)
Examples: 'Minimum capacity to ship packages in D days' (LC 1011) — binary search capacity from max(weights) to sum(weights). 'Koko eating bananas' (LC 875) — binary search eating speed. 'Split array largest sum' (LC 410) — binary search on the maximum subarray sum. 'Find nth ugly number' — binary search on value. In every case, the feasibility check runs in O(n), making the overall solution O(n log(answer_range)).
The template is always: left = minimum_possible, right = maximum_possible, Template 2 with predicate = can_do(mid).
Bisect-on-Answer: Minimum Capacity to Ship Packages
def ship_within_days(weights: list[int], days: int) -> int:
"""
LC 1011 — minimum ship capacity to deliver all packages in <= days days.
Key insight: binary search on CAPACITY, not on the array.
- If capacity C works (can ship all in <= days), C+1 also works.
- Predicate: can_ship(C) = True iff we can partition weights into
at most 'days' groups each summing <= C.
Search space: [max(weights), sum(weights)]
- Lower bound: must carry heaviest single package
- Upper bound: carry everything in one day
Time: O(n * log(sum(weights))), Space: O(1)
"""
def can_ship(capacity: int) -> bool:
days_needed, current_load = 1, 0
for w in weights:
if current_load + w > capacity:
days_needed += 1
current_load = 0
current_load += w
return days_needed <= days
left, right = max(weights), sum(weights)
while left < right:
mid = left + (right - left) // 2
if can_ship(mid):
right = mid # mid works, try smaller
else:
left = mid + 1 # mid too small
return left
Search in Rotated Sorted Array (LC 33)
Rotated arrays are the most common binary search variant in interviews. The key insight: one half is always sorted after any rotation. You can determine which half is sorted by comparing arr[left] and arr[mid].
The decision rule: if arr[left] <= arr[mid], the left half [left..mid] is fully sorted. Otherwise, the right half [mid..right] is sorted. Once you know which half is sorted, check if target falls within the sorted half's range. If yes, search that half; otherwise, search the other.
Why arr[left] <= arr[mid] and not < ? Because with duplicates (LC 81), arr[left] == arr[mid] is ambiguous — both halves might contain duplicates. For LC 33 (no duplicates), <= is correct. For LC 81 (with duplicates), you must handle arr[left] == arr[mid] == arr[right] with left++, right-- which degrades worst case to O(n).
This is a canonical example of Template 1 applied to a non-standard structure. The sorted-half insight is what interviewers are testing — state it explicitly before coding.
Rotated Sorted Array — LC 33
def search_rotated(nums: list[int], target: int) -> int:
"""
LC 33 — search in rotated sorted array. No duplicates.
Key insight: split at mid → one half is ALWAYS sorted.
Determine which half is sorted, check if target is in that half,
then binary search the appropriate half.
Time: O(log n), Space: O(1)
"""
left, right = 0, len(nums) - 1
while left <= right: # Template 1: exact target
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
if nums[left] <= nums[mid]:
# Left half [left..mid] is sorted
if nums[left] <= target < nums[mid]:
right = mid - 1 # target in sorted left half
else:
left = mid + 1 # target in right half
else:
# Right half [mid..right] is sorted
if nums[mid] < target <= nums[right]:
left = mid + 1 # target in sorted right half
else:
right = mid - 1 # target in left half
return -1
Find First and Last Position (LC 34)
LC 34 is Template 2 applied twice — once to find the leftmost occurrence and once to find the rightmost. Many candidates try to do this in one pass and create subtle bugs. Do two separate binary searches; it's cleaner and what interviewers expect.
Finding leftmost: When nums[mid] == target, don't return — set right = mid. This ensures we keep searching left. The predicate is nums[mid] >= target.
Finding rightmost: When nums[mid] == target, set left = mid + 1 to keep searching right. But Template 2's right = mid and left = mid + 1 don't directly apply here — you need a slight variant where when nums[mid] <= target, set left = mid. To avoid infinite loops, use mid = left + (right - left + 1) // 2 (round up) when left = mid is a possibility.
The safer approach: for rightmost, binary search for the first index where nums[mid] > target, then subtract 1.
Find First and Last Position — LC 34
def search_range(nums: list[int], target: int) -> list[int]:
"""
LC 34 — find first and last position of target in sorted array.
Two separate binary searches: leftmost and rightmost occurrence.
Time: O(log n), Space: O(1)
"""
def find_leftmost(target: int) -> int:
left, right = 0, len(nums) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
result = mid # record and keep searching left
right = mid - 1
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
def find_rightmost(target: int) -> int:
left, right = 0, len(nums) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
result = mid # record and keep searching right
left = mid + 1
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
return [find_leftmost(target), find_rightmost(target)]
Find Peak Element (LC 162)
Peak finding demonstrates that binary search works on unsorted arrays — the only requirement is a monotonic decision rule, not a sorted order.
The key insight: if nums[mid] < nums[mid+1], then nums[mid+1] is larger than nums[mid]. Moving right, either we eventually hit a peak (when the array stops increasing), or we reach the boundary (which is −∞ by problem definition). Either way, a peak exists to the right of mid. Conversely, if nums[mid] > nums[mid+1], a peak exists at or to the left of mid.
This is Template 2 with predicate: nums[mid] > nums[mid+1]. The loop invariant: a peak always exists within [left, right]. Every iteration preserves this invariant and shrinks the search space. Why it converges: left < right ensures mid < right, so mid+1 is always a valid index.
Common mistake: accessing nums[mid+1] without verifying mid < len(nums)-1. The loop condition left < right with mid = left + (right-left)//2 guarantees mid < right <= len(nums)-1, so mid+1 is always safe.
Find Peak Element — LC 162
def find_peak_element(nums: list[int]) -> int:
"""
LC 162 — find peak element (greater than both neighbors).
Array edges are treated as -infinity.
May return ANY peak if multiple exist.
Key: binary search on UNSORTED array using a directional decision.
If nums[mid] < nums[mid+1]: peak is to the RIGHT (move left up)
If nums[mid] > nums[mid+1]: peak is at mid or to the LEFT (move right down)
Loop invariant: a peak always exists in [left, right].
Time: O(log n), Space: O(1)
"""
left, right = 0, len(nums) - 1
while left < right: # Template 2
mid = left + (right - left) // 2
# mid < right is guaranteed, so mid+1 is always valid
if nums[mid] < nums[mid + 1]:
left = mid + 1 # peak is to the right
else:
right = mid # nums[mid] >= nums[mid+1]: peak at mid or left
return left # left == right == peak index
Median of Two Sorted Arrays (LC 4) — The O(log min(m,n)) Solution
LC 4 is typically the hardest binary search problem in interviews and is usually a 'stretch' question at Staff level. Most LC Hard solutions run O(log(m+n)) with a confusing recursive approach; the clean O(log(min(m,n))) solution binary searches on the partition point of the smaller array.
The idea: the median partitions the combined array into two equal halves. If we place a partition after index i in A (smaller array), then the partition in B must be at index (m+n+1)/2 - i to keep both halves equal. We need: A[i-1] <= B[j] and B[j-1] <= A[i] (cross-conditions for a valid partition). Binary search i on the smaller array until both conditions hold.
Why binary search on the smaller array: we binary search on i ∈ [0, m] where m = len(A). Each step is O(1), giving O(log m) = O(log(min(m,n))). If A[i-1] > B[j], i is too large → decrease. If B[j-1] > A[i], i is too small → increase.
Edge cases: when i=0 (no elements from A on left), use −∞ for A[i-1]. When i=m (all elements from A on left), use +∞ for A[i]. Handle odd total length by checking if (m+n) is odd — if so, median is max(left_max) directly.
Median of Two Sorted Arrays — LC 4
def find_median_sorted_arrays(nums1: list[int], nums2: list[int]) -> float:
"""
LC 4 — median of two sorted arrays in O(log(min(m,n))).
Binary search on partition index of the SMALLER array.
Valid partition: max(left_A, left_B) <= min(right_A, right_B)
Time: O(log(min(m,n))), Space: O(1)
"""
# Ensure nums1 is the smaller array
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
half_len = (m + n + 1) // 2 # left half size
left, right = 0, m
while left <= right:
i = left + (right - left) // 2 # partition index in nums1
j = half_len - i # partition index in nums2
# Edge: use -inf / +inf for boundaries
left_a = nums1[i - 1] if i > 0 else float('-inf')
right_a = nums1[i] if i < m else float('inf')
left_b = nums2[j - 1] if j > 0 else float('-inf')
right_b = nums2[j] if j < n else float('inf')
if left_a <= right_b and left_b <= right_a:
# Valid partition found
left_max = max(left_a, left_b)
if (m + n) % 2 == 1:
return float(left_max)
right_min = min(right_a, right_b)
return (left_max + right_min) / 2.0
elif left_a > right_b:
right = i - 1 # i too large, move left
else:
left = i + 1 # i too small, move right
raise ValueError("Input arrays are not sorted")
Binary Search Problem Taxonomy — 12 Classic Problems
| LC# | Problem | Template | Key Predicate / Condition | Difficulty |
|---|---|---|---|---|
| 704 | Binary Search | Template 1 | Find exact target in sorted array | Easy |
| 35 | Search Insert Position | Template 2 | nums[mid] >= target (first index ≥ target) | Easy |
| 33 | Search in Rotated Array | Template 1 | One half is always sorted; check which | Medium |
| 34 | First and Last Position | Template 2 × 2 | Two passes: leftmost + rightmost occurrence | Medium |
| 153 | Min in Rotated Array | Template 2 | nums[mid] > nums[right] → min is right of mid | Medium |
| 162 | Find Peak Element | Template 2 | nums[mid] < nums[mid+1] → peak is right | Medium |
| 875 | Koko Eating Bananas | Bisect-on-answer | can_eat(speed): total_hours <= H | Medium |
| 1011 | Ship Packages in D Days | Bisect-on-answer | can_ship(cap): days_needed <= D | Medium |
| 410 | Split Array Largest Sum | Bisect-on-answer | can_split(max_sum): splits_needed <= m | Hard |
| 74 | Search 2D Matrix | Template 1 (2D→1D) | Treat matrix as flattened sorted array | Medium |
| 240 | Search 2D Matrix II | Elimination | Start top-right: move left if too big, down if small | Medium |
| 4 | Median of Two Sorted Arrays | Template 1 on partition | left_a ≤ right_b AND left_b ≤ right_a | Hard |
Common Mistakes That Cost Offers
Wrong loop condition: Using left <= right with right = mid creates an infinite loop when left == right and the predicate is True — mid == left == right, right = mid, nothing changes. Always pair left < right with right = mid.
Integer overflow (non-Python): In Java/C++, (left + right) / 2 overflows for large indices. Use left + (right - left) / 2. Python ints don't overflow, but write it correctly anyway — interviewers notice.
Off-by-one in predicate direction: For Template 2, if predicate(mid) is True, you set right = mid (not mid-1). If you use right = mid - 1, you might skip the answer. If predicate(mid) is False, use left = mid + 1 (safe to skip mid). Swapping these produces wrong answers, not runtime errors — the hardest class of bug.
Wrong boundary for bisect-on-answer: For minimum-capacity problems, right = sum(weights), not max(weights). An off-by-one on the boundary produces wrong answers on edge cases (single element, all equal elements).
Interview Questions
Click to reveal answersSign in to take the Quiz
This topic has 20 quiz questions with instant feedback and detailed explanations. Sign in to unlock quizzes.
Sign in to take quiz →