leetcode / Binary search / Binary search intro

Binary search

Binary search finds a position in a sorted range by throwing away half of what is left after every comparison. The skill is not the idea but the bookkeeping: a clear invariant for what the range still guarantees, and a loop that always shrinks it. Get those right and the same few lines find a value, an insertion point, the pivot of a rotated array, or the first element that satisfies any yes-or-no test.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The invariant
  5. Finding a value, and why this form is safer
  6. Any monotone yes-or-no test
  7. Rotated arrays
  8. Worked example
  9. Implementation
  10. Variations and edge cases
  11. Complexity and tradeoffs
  12. Problems
  13. Easy
  14. Medium
  15. Hard
  16. Also uses this pattern
  17. Connections
  18. Read first
  19. Read next
  20. Check yourself
  21. References

Problem

Given a sorted array, is a value in it, and where? Scanning from the left costs O(n). The array being sorted is information you are not using: one comparison with the middle element tells you which half can hold the answer and which half cannot. Repeat on the surviving half and a million elements take about twenty comparisons. The same idea applies whenever the data has a hidden order: a rotated array is two sorted runs glued together, and a "good, good, bad, bad, bad" sequence is sorted by the yes-or-no answer even if nothing looks like numbers.

SignalPatternWhy
Sorted array, find a value or its indexClassic binary searchComparing with the middle discards half the candidates.
"Where would this value be inserted", "first element at least x"Lower boundThe answer is the first position where "value >= target" turns true.
Versions or items that are good and then badSearch the first true of a monotone predicateA predicate that flips once is a sorted sequence of falses then trues.
Sorted array rotated at an unknown pointFind the sorted halfOne of the two halves around the midpoint is always in order.
Sorted matrix with rows continuing from the previous rowSearch the flattened indexRow-major order is one sorted array in disguise.
Many queries against data sorted by time or keySearch for the last entry at or before a timeSorting once pays for O(log n) per lookup.
A required O(log n) boundBinary searchOnly halving gets below linear time on sorted input.

Intuition

It is the number-guessing game. You pick a number between 1 and 100 and say "higher" or "lower" after each guess. Guessing the middle every time is the best strategy because whatever you hear, half of the remaining numbers are gone. After seven guesses at most one number is left.

What makes this safe to code is to say in words what the range promises. In the version used here the range is half-open, from lo up to but not including hi, and it promises "the answer is somewhere in this range, and if no element qualifies the answer is hi". Every line of the loop exists to keep that sentence true while making the range smaller.

Lower-bound search for 23 in 2, 5, 8, 12, 16, 23, 38, 56, 72, 91 One row per step. Cells still inside the search range are drawn solid and the cell at the midpoint is amber. Step 1: the range is 0 to 9 and the midpoint is index 5 holding 23. Step 2: the range is 0 to 4 and the midpoint is index 2 holding 8. Step 3: the range is 3 to 4 and the midpoint is index 4 holding 16. The search ends at index 5. step 1 2 5 8 12 16 23 38 56 72 91 step 2 2 5 8 12 16 23 38 56 72 91 step 3 2 5 8 12 16 23 38 56 72 91 result 2 5 8 12 16 23 38 56 72 91

Blue cells are the live range, amber is the midpoint just examined and green the final answer. The live range starts at all 10 cells and shrinks every row.

Mechanics

The invariant

Search for the first index whose value is at least the target in the half-open range [lo, hi), starting at lo = 0 and hi = len(nums). The invariant: every index below lo holds a value less than the target, and every index at or above hi holds a value at least the target (or does not exist). The answer therefore sits in [lo, hi].

While lo < hi, take mid = (lo + hi) // 2, which is always inside the range. If nums[mid] < target, then mid and everything below it are too small: set lo = mid + 1. Otherwise mid qualifies and so does everything after it, but something earlier might also qualify: set hi = mid. Both moves keep the invariant, and both shrink the range by at least one element, so the loop ends with lo == hi, and the invariant says that index is the answer.

Finding a value, and why this form is safer

Lower bound does not ask "is this the target?" in the loop. When it ends, check once whether lo is in range and nums[lo] == target. This single shape answers three questions: membership, insertion point (lo itself, even when the value is missing) and first occurrence among duplicates. An early return on nums[mid] == target gives up the last two and invites off-by-one bugs, so use it only for plain membership.

Any monotone yes-or-no test

Nothing above needed numbers. All it used was a predicate ok(i) that is false for a prefix of the indices and true for the rest. Replace nums[mid] < target with not ok(mid) and the loop finds the first true. "First bad version" is exactly that. The next page uses the same loop over answers rather than positions.

Rotated arrays

Rotating a sorted array of distinct values makes two ascending runs, with a drop between them. For any midpoint, at least one of [lo, mid] and [mid, hi] does not contain the drop and is therefore sorted. Test which: if nums[lo] <= nums[mid] the left half is sorted. Then check whether the target lies within that sorted half's bounds; if so keep it, otherwise discard it and keep the other half. To find the minimum, compare nums[mid] with nums[hi]: if the midpoint is larger the drop is to its right, otherwise the minimum is at mid or before it.

Rotated array 15, 19, 23, 38, 2, 5, 8, 12 drawn as bars Bar heights follow the values 15, 19, 23, 38, 2, 5, 8, 12. The values rise, drop at index 4 to the minimum 2, and rise again. The minimum search visits midpoints 3, 5, 4 and ends at index 4. 15 0 19 1 23 2 38 3 2 4 5 5 8 6 12 7

Two ascending runs with a drop between them. Amber bars are midpoints the minimum search looked at; green is the minimum it found.

Worked example

Lower bound of 23 in [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]. The range is [lo, hi):

Steplohimidnums[mid]Move
1010523hi = mid
20528lo = mid + 1
335416lo = mid + 1

The search stops at index 5, and 23 equals the target, so 23 is present. Searching for 20 instead stops at index 5, the position where 20 would be inserted, and nums[5] is 23, not 20, so it is absent.

Rotated search for 5 in [15, 19, 23, 38, 2, 5, 8, 12]. This is a closed range, so the loop runs while lo <= hi:

Steplohimidnums[mid]Sorted halfMove
107338leftlo = mid + 1
24755-found

The value 5 is at index 5.

Rotated minimum for the same array. Each step compares the midpoint with the right end:

Steplohimidnums[mid]nums[hi]Move
10733812lo = mid + 1
2475512hi = mid
345425hi = mid

The minimum is 2 at index 4.

Implementation

from bisect import bisect_left


def lower_bound(nums, target):
    lo, hi = 0, len(nums)                        # answer is in [lo, hi]
    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] < target:
            lo = mid + 1
        else:
            hi = mid
    return lo


def search(nums, target):
    i = lower_bound(nums, target)
    return i if i < len(nums) and nums[i] == target else -1


def first_true(lo, hi, ok):
    """Smallest i in [lo, hi) with ok(i), or hi if none; ok must be False for a prefix, then True."""
    while lo < hi:
        mid = (lo + hi) // 2
        if ok(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo


def search_rotated(nums, target):
    lo, hi = 0, len(nums) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if nums[mid] == target:
            return mid
        if nums[lo] <= nums[mid]:                # left half is sorted
            if nums[lo] <= target < nums[mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        else:                                    # right half is sorted
            if nums[mid] < target <= nums[hi]:
                lo = mid + 1
            else:
                hi = mid - 1
    return -1


def rotated_minimum(nums):
    lo, hi = 0, len(nums) - 1
    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] > nums[hi]:
            lo = mid + 1
        else:
            hi = mid
    return nums[lo]

bisect_left(nums, target) from the standard library is lower_bound; in an interview write the loop out, and use bisect at work. Python integers do not overflow, so (lo + hi) // 2 is safe here; in a fixed-width language write lo + (hi - lo) // 2.

Variations and edge cases

  • Last position instead of first. Find the first index greater than the target (nums[mid] <= target moves right) and subtract one. This is bisect_right.
  • Matrix as one array. Search indices 0 to rows × cols − 1 and read the cell at divmod(mid, cols).
  • Search for a window, not an element. For the k closest elements, search the left edge of the window over [0, n − k] and compare how far x is from nums[mid] and nums[mid + k].
  • Duplicates in a rotated array. When nums[lo] == nums[mid] == nums[hi] you cannot tell which half is sorted, so shrink both ends by one; the worst case degrades to O(n).
  • Edge cases: an empty array (the loop never runs and lower bound returns 0), a target smaller than everything (index 0), larger than everything (index n), a single element, duplicates, and a rotation by zero, where the array is simply sorted.

Complexity and tradeoffs

  • Time: O(log n). The range loses at least half its elements per step. This array of 10 needed 3 steps.
  • Space: O(1) for the loop.
  • Baseline. A linear scan is O(n) and needs no ordering. If you search once in unsorted data, scanning beats sorting first (O(n log n)); binary search wins when the data is sorted already or searched many times.
  • When it fails. The data must have order that a single comparison can exploit. Unsorted data, or a predicate that flips more than once, breaks the invariant. A rotated array with many duplicates loses the guarantee that one half is sorted.

Problems

The home problems are the plain search, the first-true predicate, the search over a flattened matrix, the rotated variants, a time-keyed store that searches the last entry at or before a time, and the mountain array, which needs the loop twice. Every one is a version of "keep the invariant, shrink the range".

Easy

  • 35. Search Insert Position NeetCode 250
    The answer is the first index whose value is at least the target, which is exactly where a lower-bound search stops.
  • 278. First Bad Version Grind 169
    Versions are good then bad, so search for the first true of a monotone predicate.
  • 374. Guess Number Higher or Lower NeetCode 250
    The feedback says which half holds the hidden number, so the range halves after every guess.
  • 704. Binary Search Grind 169 NeetCode 150 NeetCode 250
    Keep a closed range that must contain the target and halve it by comparing the midpoint.

Medium

  • 33. Search in Rotated Sorted Array Blind 75 Grind 169 NeetCode 150 NeetCode 250
    At every midpoint one half is sorted, so check whether the target lies inside that sorted half and discard the other.
  • 74. Search a 2D Matrix Grind 169 NeetCode 150 NeetCode 250
    Treat the matrix as one sorted array of rows times columns and map each midpoint back to a row and column.
  • 81. Search in Rotated Sorted Array II NeetCode 250
    Search like the rotated case, but when the low, middle and high values are equal you cannot tell which half is sorted, so shrink both ends by one.
  • 153. Find Minimum in Rotated Sorted Array Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Compare the midpoint with the right end: if it is larger the minimum lies to its right, otherwise at or to its left.
  • 658. Find K Closest Elements Grind 169 NeetCode 250
    Binary search for the left edge of the length-k window, comparing the distances of x to the window's two possible ends.
  • 981. Time Based Key-Value Store Grind 169 NeetCode 150 NeetCode 250
    Timestamps arrive in increasing order per key, so binary search for the last one at or before the query time.

Hard

  • 1095. Find in Mountain Array NeetCode 250
    Binary search for the peak, then search the ascending side first and the descending side second, each with its own comparison.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

The loop-halving analysis comes from the complexity page. The next page applies the same first-true loop to the space of possible answers, which is the form most hard binary-search interview problems take. Trees bring the same halving to linked structures, where a binary search tree is a binary search you can update.

Check yourself

Why does the lower-bound loop use hi = mid but lo = mid + 1?

When nums[mid] is too small, mid itself is ruled out, so lo can skip past it. When it qualifies, mid might be the answer, so it has to stay inside the range as the new hi. Using hi = mid - 1 could throw away the answer, and lo = mid could loop forever once the range has two elements.

What does the loop return for a missing value, and why is that useful?

It returns the insertion point, the index where the value would go to keep the array sorted. For 20 here that is index 5, between 16 and 23.

In a rotated array, how do you know which half is sorted?

Compare nums[lo] with nums[mid]. If the left end is at most the midpoint, there is no drop between them, so the left half is ascending. Otherwise the drop is on the left and the right half must be ascending.

The first search of this page took 3 steps on 10 elements. How many would an array of about a million need?

About twenty, since 220 is just over a million and each step halves the range. Doubling the data adds one step.

References

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms: binary search as an exercise in loop invariants, and the divide-and-conquer chapter.
  • Knuth, The Art of Computer Programming, volume 3, section 6.2.1, on searching an ordered table; it notes that the first binary search was published in 1946 but a bug-free one only in 1962.
  • Bentley, Programming Pearls, column 4: writing a correct binary search with an invariant.
  • Sedgewick and Wayne, Algorithms, section 3.1: binary search in an ordered symbol table.
  • Skiena, The Algorithm Design Manual: the section on binary search and bisection.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.