Patterns/Binary Search

Binary Search

Top interview pattern
85 min read
Updated June 2026
What you'll learn
  • Choose between exact-match and boundary-finding templates based on whether the problem asks for existence or position, and understand how a single operator change affects convergence
  • Solve rotated-array search by identifying which half is sorted (one always is) and checking whether the target falls within that half's range
  • Apply binary search on the answer space for optimization problems where the feasibility function is monotonic
  • Diagnose and fix the five specific boundary bugs (wrong mid formula, inclusive/exclusive mismatch, infinite loop, premature exit, answer elimination) that break binary search implementations

Binary search halves a search space with each comparison. The halving itself is not where learners fail. The difficulty is in boundary choices: whether to use left <= right or left < right, whether right should be mid or mid - 1, and whether the search space is inclusive or exclusive. Each of these choices must be consistent with the others, or the loop either skips the answer or never terminates. This guide teaches the five forms of binary search, the reasoning behind each boundary choice, and how to verify that a loop terminates correctly.

What Binary Search Actually Does

A linear scan checks each element and learns nothing from each comparison that helps skip later elements. If the target is at the end, every element before it is examined.

Linear Scan
1def linear_search(arr, target):
2 for i in range(len(arr)):
3 if arr[i] == target:
4 return i
5 return -1

Binary search eliminates half the candidates with each comparison. When arr[mid] < target in a sorted array, every index to the left of mid also holds a value less than target. The comparison proves that the entire left half cannot contain the answer, so the search discards it.

Binary Search
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3
4 while left <= right:
5 mid = left + (right - left) // 2
6
7 if arr[mid] == target:
8 return mid
9 elif arr[mid] < target:
10 left = mid + 1
11 else:
12 right = mid - 1
13
14 return -1 # Not found

This is the exact search form. It returns the moment it finds target, or -1 if target is absent. But many problems need something different: finding the first occurrence, the insertion point, or the boundary where some condition changes. Those require a different form.

How the Search Space Shrinks

Binary search maintains an invariant: if the answer exists, it lies within [left, right]. Each comparison proves that half the remaining elements cannot be the answer, allowing them to be discarded. Every update to left or right must preserve this invariant.

The real requirement for binary search is not a sorted array. It is a monotonic decision structure: a condition that transitions from false to true (or vice versa) as the search space is traversed. Binary search finds the transition point. Sorting is one way to create this structure, but not the only way.

Key Insight
Monotonic Properties Enable Binary Search
The condition does not need to involve array elements at all. In peak finding, the condition is "arr[i] > arr[i+1]" which transitions from false to true at the peak. In rotated array minimum search, the condition is "arr[i] <= arr[last]" which transitions from false to true at the minimum. In binary search on the answer, the condition is a feasibility function like "can_finish(speed)" which transitions from false to true as the candidate value increases. Each of these creates a monotonic boundary that binary search can locate.

Three Kinds of Search Space

Index space. The answer is a position in an array. The search space is [0, n-1] or [0, n). Problems like "find target in sorted array" and "find peak element" fall here. The giveaway is that the problem provides an array and asks for a position.

Value space. The answer is a numeric value, not an index. The search space is [min_possible, max_possible]. Integer square root and "find smallest divisor" are value space problems. The giveaway is that the problem asks for a value satisfying some constraint, not a position.

Answer space. The answer is the optimal value of some objective. The search space is the range of possible answers to the optimization. Koko eating bananas, split array largest sum, and capacity to ship packages all fit this category. The giveaway is phrasing like "minimize the maximum" or "maximize the minimum." For each candidate answer, a feasibility function determines whether that answer is achievable.

ProblemSearch SpaceWhat the Answer Represents
Find target in array[0, n-1] (indices)Index where arr[i] == target
Koko eating bananas[1, max(piles)] (speeds)Minimum speed to finish in h hours
Split array largest sum[max(arr), sum(arr)]Minimum achievable max-subarray-sum

First Walkthrough

Before learning the forms, trace through a concrete example to see how the search space shrinks. The animation below searches for target = 9 in [1, 3, 5, 7, 9, 11, 13, 15].

Binary Search · target = 9

Left
0
Right
7
Mid
arr[mid]
Compare
Found
arr[]target: 91031527394115136157

Target = 9. The array is sorted, so every comparison can safely eliminate half the search space.

At each step, the comparison proves that half the remaining elements cannot be the target. When arr[mid] is less than 9, sorted order guarantees every element left of mid is also less than 9, so the left half is safely discarded. The search space shrinks from 8 to 4 to 2 to 1. Found in 3 comparisons. A linear scan of 8 elements could need 8.

Form 2: Lower Bound

Lower bound finds the first position where arr[i] >= target. When target exists, this is the first occurrence. When target is missing, this is where it would be inserted to maintain sorted order. This form treats the search space as [left, right) where left is inclusive and right is exclusive.

Lower and Upper Bound Positions
1# Array: [1, 2, 2, 2, 3, 4]
2# Index: 0 1 2 3 4 5
3
4# lower_bound(2) = 1 (first 2)
5# upper_bound(2) = 4 (first element > 2)
6
7# Count of 2s = upper_bound - lower_bound = 4 - 1 = 3
Form 2: Lower Bound
1def lower_bound(arr, target):
2 left, right = 0, len(arr)
3
4 while left < right:
5 mid = left + (right - left) // 2
6
7 if arr[mid] < target:
8 left = mid + 1
9 else:
10 right = mid
11
12 return left

Why right = len(arr)? The boundary might be past the end. If all elements are less than target, the insertion point is index len(arr). This must be a valid answer, so the search space must include it.

Why left < right (not <=)? When left == right, the search space [left, right) is empty. No candidates remain. The boundary is at index left.

Why right = mid (not mid - 1)? When arr[mid] >= target, mid might BE the boundary (the first element >= target). Setting right = mid shrinks the space but keeps mid reachable. When the loop terminates with left == right, that index is the boundary. Using mid - 1 would skip past the boundary entirely.

When arr[mid] equals target, you found a match, but possibly not the first one. The condition arr[mid] < target is false, so the else branch runs: right = mid. This keeps mid as a candidate while continuing leftward. The search keeps going left until left meets right at the leftmost occurrence.

Common Mistake
One Character Changes the Convergence Point
For lower bound (first element >= target), use arr[mid] < target. For upper bound (first element > target), use arr[mid] <= target. The difference is one character, but it determines whether the search settles at the first match or pushes past all matches.

Form 3: Upper Bound

Upper bound finds the first position where arr[i] > target. When target exists, this lands one position after the last occurrence. Use upper bound when you need "last occurrence" (subtract 1 from the result) or "count of target" (upper minus lower).

Form 3: Upper Bound
1def upper_bound(arr, target):
2 left, right = 0, len(arr)
3
4 while left < right:
5 mid = left + (right - left) // 2
6
7 if arr[mid] <= target:
8 left = mid + 1
9 else:
10 right = mid
11
12 return left

When arr[mid] equals target, upper bound wants to get past all matches. The condition arr[mid] <= target is true, so the if branch runs: left = mid + 1. This excludes mid and continues right. The search keeps going right until it lands on the first element strictly greater than target, or reaches the end.

Interview Tip
Counting Occurrences in One Step
The number of times target appears is upper_bound(target) - lower_bound(target). The last occurrence is at upper_bound(target) - 1. Both use the same boundary search form with different comparison operators.

Form 5: Binary Search on Answer

Binary Search on Answer · piles = [3, 6, 7, 11], h = 8

Left
1
Right
11
k
Hours
Limit
8
Feasible
speed k1234567891011

Find minimum speed k to finish in ≤8 hours. Search space: k ∈ [1, 11].

Rather than searching within an array, this form searches the space of possible answers. It solves optimization problems where the answer has a monotonic feasibility boundary.

Direct approaches to these optimization problems often require exponential time. Enumerating all possible splits or assignments grows combinatorially. The insight is to flip the question: instead of asking "what is the optimal answer?", ask "given a candidate answer X, is X achievable?" This second question typically has a simple O(n) greedy solution.

The answer space forms a monotonic boundary. For minimization problems like "minimum speed to finish," small values fail and large values succeed. The boundary between failure and success is exactly the answer. Binary search locates this boundary in O(log(range)) feasibility checks.

Recognition. The problem asks to "minimize the maximum" or "maximize the minimum" of some quantity. Another signal is when the problem has a natural feasibility check: "can we achieve X using at most k resources?"

Three Steps: Define Range, Check Feasibility, Search

1. Define the answer range. Determine the smallest and largest possible answers. The minimum is usually a constraint (the largest element must fit). The maximum is usually trivial (the entire sum).

2. Write a feasibility function. For a candidate answer X, the function returns true if X is achievable and false otherwise. This function typically uses a greedy simulation.

3. Binary search for the boundary. The answer space has a monotonic property: once a candidate works, all easier candidates also work. Find the boundary where infeasible becomes feasible (or vice versa).

Koko Eating Bananas

Given piles of bananas and h hours, find the minimum eating speed k (bananas per hour) to finish all piles within h hours.

Koko Eating Bananas
1def min_eating_speed(piles, h):
2 def can_finish(k):
3 hours = 0
4 for pile in piles:
5 hours += (pile + k - 1) // k # Ceiling division
6 return hours <= h
7
8 left, right = 1, max(piles)
9
10 while left < right:
11 mid = left + (right - left) // 2
12
13 if can_finish(mid):
14 right = mid # Try smaller k
15 else:
16 left = mid + 1 # Need larger k
17
18 return left

(pile + k - 1) // k computes the ceiling of pile/k using integer arithmetic. Adding k-1 before dividing ensures any remainder rounds up. This avoids floating-point precision issues with large numbers.

The answer space [1, max(piles)] has a monotonic property: small k means cannot finish (false), large k means can finish (true). Binary search finds the boundary where false becomes true.

Minimizing the Maximum

Split an array into m subarrays to minimize the largest subarray sum. Binary search on the answer: the search space is [max(nums), sum(nums)]. The lower bound is max(nums) because each element must belong to exactly one subarray, so no subarray sum can be less than the largest element.

The feasibility function greedily assigns elements to subarrays. If the current sum exceeds the candidate, start a new subarray. If you need more than m subarrays, the candidate is too small.

Split Array Largest Sum
1def split_array(nums, m):
2 def can_split(max_sum):
3 count, current = 1, 0
4 for num in nums:
5 if current + num > max_sum:
6 count += 1
7 current = num
8 else:
9 current += num
10 return count <= m
11
12 left, right = max(nums), sum(nums)
13 while left < right:
14 mid = left + (right - left) // 2
15 if can_split(mid):
16 right = mid
17 else:
18 left = mid + 1
19 return left
Key Insight
Inverted Feasibility
Minimization and maximization problems have inverted feasibility boundaries but use the same binary search structure. In minimization, small values fail and large values succeed, so the search finds the smallest feasible value by setting right = mid when feasible. In maximization, large values fail and small values succeed, so the search finds the largest feasible value by setting left = mid when feasible (with mid biased rightward to avoid infinite loops).

Why the Runtime Works

Each comparison eliminates half the remaining candidates. After k comparisons, n/2k candidates remain. When n/2k = 1, the search terminates. Solving for k gives k = log2(n).

Array SizeComparisons Needed
10~4
100~7
1,000~10
1,000,000~20
1,000,000,000~30

Each doubling of the array size adds only one comparison. A billion elements require only about 30 comparisons.

For binary search on the answer, the runtime is O(log(range) * C) where C is the cost of the feasibility check. If the feasibility check is O(n), the total is O(n log(range)). For Koko eating bananas with max(piles) = 109, this is about 30 feasibility checks, each scanning the array once.

Binary search on a sorted array uses O(1) space. Binary search on the answer uses O(1) space beyond the input. The space cost is always the feasibility function's requirements, not the search itself.

When This Pattern Breaks

No monotonic property. If no condition transitions from false to true across the search space, binary search cannot eliminate half the candidates. Searching for a value in an unsorted array with no local structure has no monotonic property. A comparison at mid tells you nothing about the values on either side.

Duplicates in rotated arrays. When arr[left] == arr[mid] == arr[right], the algorithm cannot determine which half is sorted. It can only shrink the search space by one element instead of half, degrading worst-case performance to O(n). This is unavoidable when duplicates exist.

Continuous answer space. When the answer is a floating-point value (such as "find the square root of 2"), the loop cannot use left < right for termination because floating-point equality is unreliable. Use right - left > epsilon or run a fixed number of iterations (100 iterations gives precision to 2-100, far beyond what floating-point can represent).

Non-monotonic feasibility. If the feasibility function does not transition cleanly from false to true (or vice versa), binary search may converge to the wrong point. For example, if some speeds can finish and some faster speeds cannot (due to non-uniform pile sizes with a different constraint structure), the boundary is not monotonic and binary search does not apply.

Choosing the Right Form

The first question is: what is the search looking for? An exact value that may or may not exist calls for exact search with an explicit "found" return. A boundary position (first occurrence, insertion point, first satisfying some condition) calls for lower or upper bound. An optimal value with a feasibility check calls for binary search on the answer.

A common mistake is using exact search when boundary finding is needed. If the problem asks for "the first position where X holds" or "the smallest value satisfying Y," the answer may not be at any specific array element that matches target. The search narrows to a boundary, not a match.

Another mistake is confusing lower and upper bound. Both use the same loop structure, differing only in the comparison operator. Lower bound finds where arr[i] >= target first becomes true. Upper bound finds where arr[i] > target first becomes true. Using the wrong one shifts the result by one position.

Problem SignalLikely FormExample Problem
Sorted array, does target exist?Exact searchBinary Search
Sorted array + first/last occurrenceLower/upper boundFind First and Last Position
Sorted but rotatedRotated searchSearch in Rotated Sorted Array
Unsorted but local slope existsPeak search (boundary)Find Peak Element
Minimize/maximize some quantityBinary search on answerKoko Eating Bananas
Problem PatternFormTemplate
Find target in sorted arrayExact searchleft <= right, return index when found
First occurrence / leftmostLower boundleft < right, arr[mid] < target to go right
Last occurrence / rightmostUpper bound - 1left < right, arr[mid] <= target to go right
Search in rotated arrayRotated searchIdentify sorted half, check if target in range
Minimize the maximumAnswer space (min)Binary search on candidate answers with greedy feasibility
Maximize the minimumAnswer space (max)Same structure, inverted feasibility direction
ProblemMonotonic Property
Find target in sorted arrayarr[i] >= target (false then true)
Find peak elementarr[i] > arr[i+1] (false then true at peak)
Koko eating bananascan_finish(speed) (false then true as speed increases)
Find minimum in rotated arrayarr[i] <= arr[last] (false then true at minimum)

Practice Problems

Binary search problems progress from basic sorted array searches to complex answer-space patterns. The skill is recognizing the monotonic property that enables binary search.

Tier 1: Foundational

ProblemDifficultyApproach
Binary SearchEasyThe template problem. Compare mid to target: equal = found, less = search right, greater = search left. Loop until left > right.
Search Insert PositionEasyFind where target would be inserted. This is lower bound: when loop ends, left points to the first element >= target.
First Bad VersionEasyBinary search on boolean condition. If mid is bad, answer is mid or earlier. If good, answer is later. Classic lower bound pattern.
Sqrt(x)EasyFind largest k where k*k <= x. Upper bound variant. If mid*mid <= x, save mid and search higher. Watch for integer overflow: use mid <= x/mid.

Tier 2: Lower and Upper Bound

ProblemDifficultyApproach
Find First and Last PositionMediumTwo binary searches: lower bound (first occurrence) and upper bound - 1 (last occurrence). Or find lower bound, then search for target+1 and subtract 1.
Search a 2D MatrixMediumTreat 2D matrix as 1D sorted array. Index i maps to row i/cols, col i%cols. Single binary search on virtual 1D array.
Find Smallest Letter Greater Than TargetEasyUpper bound: find first letter strictly greater than target. Letters wrap around, so if all <= target, return first letter.
Count Negative Numbers in Sorted MatrixEasyEach row sorted: binary search for first negative in each row. Or use staircase search from top-right corner.

Tier 3: Rotated Array

ProblemDifficultyApproach
Search in Rotated Sorted ArrayMediumOne half is always sorted. Check which half is sorted, then check if target is in that sorted half. Eliminate the other half.
Search in Rotated Sorted Array IIMediumDuplicates break the sorted-half detection when nums[left] == nums[mid]. Handle by incrementing left. Worst case O(n).
Find Minimum in Rotated Sorted ArrayMediumMinimum is where the rotation happened. If nums[mid] > nums[right], min is in right half. Otherwise in left half including mid.
Find Minimum in Rotated Sorted Array IIHardWith duplicates, when nums[mid] == nums[right], can't determine which half. Decrement right by 1. Worst case O(n).

Tier 4: Peak Element

ProblemDifficultyApproach
Find Peak ElementMediumIf nums[mid] < nums[mid+1], peak is to the right (ascending). Otherwise peak is at mid or left. Move toward the higher neighbor.
Peak Index in a Mountain ArrayMediumGuaranteed single peak. Same logic: if ascending at mid, go right. If descending, go left. Peak is where direction changes.
Find in Mountain ArrayHardFirst find peak with binary search. Then binary search left half (ascending) and right half (descending) separately.

Tier 5: Binary Search on Answer

ProblemDifficultyApproach
Koko Eating BananasMediumBinary search on eating speed k. For each k, check if she can finish in h hours. Monotonic: higher speed = fewer hours. Find minimum valid k.
Capacity To Ship PackagesMediumBinary search on ship capacity. For each capacity, greedily simulate shipping. Monotonic: higher capacity = fewer days. Find minimum capacity for d days.
Split Array Largest SumHardBinary search on the maximum subarray sum. For each max, greedily count splits needed. Monotonic: higher max = fewer splits. Find minimum max for m splits.
Minimize Max Distance to Gas StationHardBinary search on the maximum distance. For each max distance, count stations needed. Monotonic: larger max = fewer stations. Find minimum max for k stations.
Magnetic Force Between Two BallsMediumBinary search on minimum force (distance). For each distance, greedily place balls. Monotonic: smaller distance = more balls fit. Find maximum distance for m balls.

Reference Templates

Use when the problem asks whether a specific target exists and should return immediately on equality.

Exact Search
1def exact_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left <= right:
4 mid = left + (right - left) // 2
5 if arr[mid] == target: return mid
6 elif arr[mid] < target: left = mid + 1
7 else: right = mid - 1
8 return -1

Use when the problem asks for the first position where a condition becomes true.

Lower Bound
1def lower_bound(arr, target):
2 left, right = 0, len(arr)
3 while left < right:
4 mid = left + (right - left) // 2
5 if arr[mid] < target: left = mid + 1
6 else: right = mid
7 return left

Use when the problem asks for the first position strictly past all matching values.

Upper Bound
1def upper_bound(arr, target):
2 left, right = 0, len(arr)
3 while left < right:
4 mid = left + (right - left) // 2
5 if arr[mid] <= target: left = mid + 1
6 else: right = mid
7 return left

Use when the array is sorted but rotated and one half is always sorted.

Rotated Search
1def search_rotated(nums, target):
2 left, right = 0, len(nums) - 1
3 while left <= right:
4 mid = left + (right - left) // 2
5 if nums[mid] == target: return mid
6 if nums[left] <= nums[mid]:
7 if nums[left] <= target < nums[mid]: right = mid - 1
8 else: left = mid + 1
9 else:
10 if nums[mid] < target <= nums[right]: left = mid + 1
11 else: right = mid - 1
12 return -1

Use when the answer is a value and feasibility is monotonic.

Binary Search on Answer
1def search_on_answer(lo, hi, feasible):
2 while lo < hi:
3 mid = lo + (hi - lo) // 2
4 if feasible(mid):
5 hi = mid
6 else:
7 lo = mid + 1
8 return lo

Use when the rotation point is the answer, not the target value.

Find Minimum in Rotated Array
1def find_min_rotated(nums):
2 left, right = 0, len(nums) - 1
3 while left < right:
4 mid = left + (right - left) // 2
5 if nums[mid] > nums[right]: left = mid + 1
6 else: right = mid
7 return nums[left]

Use when the array is unsorted but local slope guarantees a peak direction.

Peak Element
1def find_peak(nums):
2 left, right = 0, len(nums) - 1
3 while left < right:
4 mid = left + (right - left) // 2
5 if nums[mid] < nums[mid + 1]: left = mid + 1
6 else: right = mid
7 return left

Form Recap

Exact search uses inclusive bounds [left, right] with left <= right. Each iteration either finds the target and returns, or excludes mid with mid + 1 or mid - 1. The loop exits with zero candidates when left passes right, meaning the target is absent.

Lower bound finds the first position where arr[i] >= target. When arr[mid] equals target, the search continues left because an earlier match may exist. Upper bound finds the first position where arr[i] > target by changing the comparison from < to <=. Both use the half-open search space [left, right) with left < right.

Rotated search identifies which half is sorted by comparing arr[left] with arr[mid]. The target is checked against the sorted half's range. If it falls within that range, search that half. Otherwise search the other half. Duplicates can break the sorted-half detection and degrade worst-case performance to O(n).

Binary search on the answer inverts the optimization question into a feasibility question. The answer space has a monotonic boundary between infeasible and feasible candidates. A greedy feasibility check at each midpoint determines which half to eliminate. The structure is identical to lower bound search, but the search space is the range of possible answers rather than array indices.

Rebuilding the Pattern

When binary search feels unclear after time away, do not start by remembering whether to use <= or <. Start by identifying what is being searched and what condition splits the search space.

If the problem asks whether a specific value exists, the search is exact. If the problem asks for the first position satisfying a condition, the search is a boundary search. If the problem asks to minimize or maximize some quantity, the search is on the answer space. That identification determines the form, and the form determines the boundary choices.

The boundary choices must be internally consistent. Inclusive search space [left, right] pairs with left <= right, mid + 1, and mid - 1. Half-open search space [left, right) pairs with left < right, mid + 1, and right = mid. Mixing conventions produces off-by-one errors or infinite loops.

The most common source of confusion is the comparison operator in boundary search. Lower bound uses arr[mid] < target because when the value equals target, the search must continue left to find earlier matches. Upper bound uses arr[mid] <= target because when the value equals target, the search must push past all matches. The difference is one character, but it determines where the search converges.

Before submitting, trace the two-element case. With left and right adjacent, verify that mid is computed correctly and that both branches make progress. If either branch leaves both pointers unchanged, the loop does not terminate.

Check Your Understanding

Use these questions to check whether you can reason through the pattern without looking at the template.

Take Assessment

11 questions covering all five binary search forms.

Practice this pattern

Apply the guide to complete interview problems with explanations and code.

Learn Binary Search in a guided sequence

The Interview Course connects this pattern to its prerequisites, worked lessons, and progressively harder problems.

Explore the course