Skip to main content
DSA·Intermediate

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)).

40 min read 18 sections 5 interview questions
Binary SearchTwo PointersDivide and ConquerLeetCodeSearch Space ReductionBisectRotated ArrayPeak ElementQuickselectMonotonic PredicateOff-by-OneTemplate

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.

IMPORTANT

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

01

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.

02

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.

03

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

Rendering diagram...

Template 2: First Index Where Predicate Is True

pythontemplate2_predicate.py
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:

  1. The question asks for a minimum or maximum value
  2. You can write a feasibility function can_do(X) that checks whether X works in O(n) or O(n log n)
  3. 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

pythonship_packages.py
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

pythonsearch_rotated.py
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

pythonfirst_last_position.py
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

pythonpeak_element.py
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

pythonmedian_two_arrays.py
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#ProblemTemplateKey Predicate / ConditionDifficulty
704Binary SearchTemplate 1Find exact target in sorted arrayEasy
35Search Insert PositionTemplate 2nums[mid] >= target (first index ≥ target)Easy
33Search in Rotated ArrayTemplate 1One half is always sorted; check whichMedium
34First and Last PositionTemplate 2 × 2Two passes: leftmost + rightmost occurrenceMedium
153Min in Rotated ArrayTemplate 2nums[mid] > nums[right] → min is right of midMedium
162Find Peak ElementTemplate 2nums[mid] < nums[mid+1] → peak is rightMedium
875Koko Eating BananasBisect-on-answercan_eat(speed): total_hours <= HMedium
1011Ship Packages in D DaysBisect-on-answercan_ship(cap): days_needed <= DMedium
410Split Array Largest SumBisect-on-answercan_split(max_sum): splits_needed <= mHard
74Search 2D MatrixTemplate 1 (2D→1D)Treat matrix as flattened sorted arrayMedium
240Search 2D Matrix IIEliminationStart top-right: move left if too big, down if smallMedium
4Median of Two Sorted ArraysTemplate 1 on partitionleft_a ≤ right_b AND left_b ≤ right_aHard
⚠ WARNING

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 answers
Test your knowledge

Sign 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 →