← All patterns
Foundation · BS

Binary Search

Search sorted data and monotonic answer spaces.

5lessons
14worked problems
Freefull access
01 / 05

Introduction

According to "Programming Pearls" by Jon Bentley, only 10% of professional software engineers can write a correct binary search - one of the fundamental algorithms you first learn. Common mistakes include integer overflow, off by 1 errors, and infinite loops (time limit exceeded). Now is the time to finally master this method once and for all.

Context:
Consider the problem of finding a target number in a sorted array.

A linear scan takes O(n), however this is wasteful. Instead consider initializing a range of [left, right] = [0, len(arr)-1]. The middle number is simply (left+right)//2. If it is >= our target we can recurse on the left half (including this middle number, so right = mid, hence range is now [left, mid]). Otherwise, it is less than our target so we know the target cannot be in the left half of the array, so we can recurse solely on the right half (excluding this middle number, so left = mid+1, hence range is now [mid+1, right]). We recurse until left == right, meaning left is the index of target. This takes O(logn) time - a significant improvement.

Intuitively you want to imagine your range [left, right] shrinking by a factor of two each iteration, until left == right meaning your range has size exactly 1, and so this last value must be what you are looking for.

KEY INSIGHT

Although most commonly binary search is used to search over arrays, note that as long as our boolean "check" function (how we are deciding whether to recurse left or recurse right) is MONOTONIC on our range [left, right], binary search can be applied. (Monotonic boolean function means of the form of all Falses then all Trues or vice versa)

WALKTHROUGH

Sample Walkthrough

[1,2,3,4,5], target = 4

  1. Left = 0, right = 4. Mid = 2. Arr[mid] < target, so we recurse on the right half by setting left = mid+1.
  2. Left = 3, right = 4. Mid = 3. Arr[mid] >= target, so we recurse on the left half by setting right = mid.
  3. Left = 3, right = 3. Left == right so we stop the iteration. Index 3 is our answer, and indeed arr[3] = 4.
02 / 05

2 Templates for Binary Search

There are two main templates depending on whether you're searching for a minimum or maximum value.

INSIGHT

Key: When solving binary search problems, characterize them in point form:
1. Min or max template
2. Initialize range: Left = ?, right = ?
3. Check function: check(mid) = ?
4. (perhaps some pre/post processing)

With this point form you should immediately know what the code looks like!

1. Initialize left, right correctly

You will need to initialize left, right correctly to the exact range of values you want to search over. [left, right] = the SOLUTION space. For arrays, often left = 0, right = len(arr)-1 (the min/max indices).

2. Why mid = left + (right-left)//2?

Instead of mid = (left+right)//2, this handles overflow, as it is possible for left+right to be > INT_MAX.

3. Why different mid values in Max vs Min?

This solves the INFINITE LOOP problem. When we set left = mid, it is possible for the while loop to never terminate. Consider [1,2] with max template and mid = (left+right)//2, left = 0, right = 1. If check(0) is true, we keep setting left = mid = 0, and our range [0,1] never decreases. Hence we need to take the ceiling to round up.

4. Why return left?

After the while loop, left == right so we can return either left or right.

5. Why different left/right updates in min vs max?

Min case: …FFFTTT… If check(mid) is true and we want the minimum true, mid might be the answer but we should still check left. Hence right = mid. Otherwise left = mid + 1.

Max case: …TTTFFF… If check(mid) is true and we want the max true, mid might be the answer but we should still check right. Hence left = mid. Otherwise right = mid - 1.

6. Recursive form

We can also write these recursively. Correctness follows inductively.

TimeO(log(right-left))
SpaceO(1)

1. Min Template - Searching for the minimum

Visualization:

FFF…FFFTTTT…TTTT [function range] (monotonically increasing)
0123… k … n [function domain]

We want the first T after all the F's (index k).

pythonREFERENCE
def binarySearchMin(...):
    left, right = x, y
    while left < right:
        mid = left + (right-left)//2
        if check(mid):
            right = mid
        else:
            left = mid + 1
    return left

2. Max Template - Searching for the maximum

Visualization:

TTT…TTTFFFF…FFFF [function range] (monotonically decreasing)
0123… k … n [function domain]

We want the last T before all the F's (index k).

pythonREFERENCE
def binarySearchMax(...):
    left, right = x, y
    while left < right:
        mid = ceil(left + (right-left)/2)
        if check(mid):
            left = mid
        else:
            right = mid - 1
    return left
05 / 05

Binary Search on Answer

Instead of searching in an array, binary search over the answer space. Key insight: if we can verify whether a candidate answer works in O(n), we can find the optimal answer in O(n log k) where k is the answer range.

Pattern recognition:
1. Problem asks for minimum/maximum of something
2. The verification problem is easy (given candidate k, can check if it works)
3. The answer has monotonic feasibility (if k works, k+1 also works, or vice versa)

WORKED PROBLEMS6
01Koko Eating BananasMedium

Find minimum eating speed k to finish all banana piles in h hours.

pythonREFERENCE
def minEatingSpeed(self, piles: List[int], h: int) -> int:
    def check(k):
        return sum(ceil(p / k) for p in piles) <= h

    left, right = 1, max(piles)
    while left < right:
        mid = left + (right - left) // 2
        if check(mid):
            right = mid
        else:
            left = mid + 1
    return left
TimeO(n log(max(piles)))
SpaceO(1)
WHY IT WORKS

Min template. Search space is [1, max(piles)]. Check: can we finish in <= h hours at speed k?

02Maximum Candies Allocated to K ChildrenMedium

Find maximum candies per child when dividing piles among k children.

pythonREFERENCE
def maximumCandies(self, candies: List[int], k: int) -> int:
    def check(size):
        return sum(c // size for c in candies) >= k

    left, right = 0, max(candies)
    while left < right:
        mid = ceil(left + (right - left) / 2)
        if check(mid):
            left = mid
        else:
            right = mid - 1
    return left
TimeO(n log(max(candies)))
SpaceO(1)
WHY IT WORKS

Max template. Same pattern as Koko but want maximum instead of minimum.

03Minimum Time to Complete TripsMedium

Find minimum time for buses to complete totalTrips. time[i] = trip duration for bus i.

pythonREFERENCE
def minimumTime(self, time: List[int], totalTrips: int) -> int:
    def check(t):
        return sum(t // bus for bus in time) >= totalTrips

    left, right = 1, min(time) * totalTrips
    while left < right:
        mid = left + (right - left) // 2
        if check(mid):
            right = mid
        else:
            left = mid + 1
    return left
TimeO(n log(min(time) × totalTrips))
SpaceO(1)
WHY IT WORKS

Min template. Upper bound: slowest single bus completes all trips.

04Split Array Largest SumHard

Split array into k subarrays to minimize the maximum subarray sum.

pythonREFERENCE
def splitArray(self, nums: List[int], k: int) -> int:
    def check(max_sum):
        chunks, curr = 1, 0
        for num in nums:
            if num > max_sum:
                return False
            if curr + num > max_sum:
                chunks += 1
                curr = num
            else:
                curr += num
        return chunks <= k

    left, right = max(nums), sum(nums)
    while left < right:
        mid = left + (right - left) // 2
        if check(mid):
            right = mid
        else:
            left = mid + 1
    return left
TimeO(n log(sum(nums)))
SpaceO(1)
WHY IT WORKS

Min template. Search space: [max(nums), sum(nums)]. Check: can we split into <= k chunks where each <= mid?

05Divide ChocolateHard

Cut chocolate into k+1 pieces for you and k friends. You get the minimum piece. Maximize your piece.

pythonREFERENCE
def maximizeSweetness(self, sweetness: List[int], k: int) -> int:
    def check(min_sweet):
        pieces, curr = 0, 0
        for s in sweetness:
            curr += s
            if curr >= min_sweet:
                pieces += 1
                curr = 0
        return pieces >= k + 1

    left, right = 1, sum(sweetness)
    while left < right:
        mid = ceil(left + (right - left) / 2)
        if check(mid):
            left = mid
        else:
            right = mid - 1
    return left
TimeO(n log(sum(sweetness)))
SpaceO(1)
WHY IT WORKS

Max template. Opposite of Q410 - this is MAX of the MIN. Greedy check: can we make k+1 pieces each with sum >= mid?

06Maximum Average Subarray IIHard

Find contiguous subarray of length >= k with maximum average.

pythonREFERENCE
def findMaxAverage(self, nums: List[int], k: int) -> float:
    def check(avg):
        # Transform: can we find subarray of len >= k with sum >= 0?
        # After subtracting avg from each element
        min_prefix = float('inf')
        prefix = 0
        lagging_prefix = 0

        for i in range(len(nums)):
            prefix += nums[i] - avg
            if i >= k - 1:
                if i == k - 1:
                    min_prefix = 0
                if prefix >= min_prefix:
                    return True
                lagging_prefix += nums[i - k + 1] - avg
                min_prefix = min(min_prefix, lagging_prefix)
        return False

    left, right = min(nums), max(nums)
    while right - left >= 1e-5:
        mid = left + (right - left) / 2
        if check(mid):
            left = mid
        else:
            right = mid
    return left
TimeO(n log((max-min)/ε))
SpaceO(1)
WHY IT WORKS

Binary search on the average value. Key insight: subtract candidate avg from all elements, then find subarray of len >= k with sum >= 0. Uses prefix sum with delayed minimum tracking.

NEXT PATTERNLinked List