◐ ai-teacher

🧩 Data Structures & Algorithms · Chapter 10

Binary search

A million sorted items, and you can find any one in 20 looks. The catch: get one +1 wrong and it loops forever.

Study this lesson free in the app →

The basics

Binary search finds a value in a sorted array by checking the middle element and throwing away the half that cannot contain the answer — again and again until it finds the value or runs out of range.

The problem it solves: a linear scan checks items one by one, so a million items means up to a million checks. Because the data is sorted, one comparison with the middle tells you which half the target is in, so each step halves the work: a million items need at most 20 checks.

The mental model: the guess-my-number game. "I'm thinking of a number from 1 to 100." Guess 50; "higher" — now it's 51–100. Guess 75; "lower". You never need more than 7 guesses.

Cost: O(log n) time. O(1) extra space for the loop version; O(log n) for the recursive one (its stack).

Words you must know 5

Sorted precondition
without sorted order, discarding a half is a guess, not a deduction
lo / hi
the ends of the range that may still hold the target
mid
the middle index, (lo + hi) // 2
Invariant
"if the target exists, it is inside a[lo..hi]", true before every step
Insertion point
where a missing value would go to keep the array sorted (bisect_left)

Explained

Linear search on a billion sorted records: up to a billion comparisons.

Binary search on the same records: 30.

The difference is not a faster computer. It's using the one fact you already paid for — the data is sorted — to throw away half of what's left with every look.

Guess my number

A friend thinks of a number between 1 and 100.

You say 50. "Higher." Every number from 1 to 50 is gone in one question. You say 75. "Lower." Now it's 51–74. You say 62. "Higher." 63–74…

You never need more than 7 guesses, because 100 → 50 → 25 → 13 → 7 → 4 → 2 → 1.

Now imagine your friend lies about the order — the numbers are written on cards in a random pile. "Higher" tells you nothing about which cards to discard. That's why binary search demands sorted input.

The invariant does the thinking

Hold one sentence true before every step:

If the target is in the array, it is somewhere in a[lo..hi].

The +1 and -1 aren't magic: mid has been checked, so it leaves the range. Each step at least halves the range, so after k steps at most n / 2ᵏ remain → O(log n) steps.

Found in four looks

Ten items, target 40. Middle is 24 (too small) → right half. Middle is 52 (too big) → left part. Then 31, then 40. Four comparisons; a linear scan would take seven.

Not there — and where it would go

Target 20 isn't in the array. Watch the range shrink — 24, 8, 12, 19 — until lo passes hi. lo stops at index 4, between 19 and 24: exactly where 20 would be inserted. That's the insertion point bisect_left returns.

The library, and the template that generalises

import bisect

a = [3, 8, 12, 19, 24, 31, 40, 52, 67, 75]
bisect.bisect_left(a, 20)    # 4  → where 20 would go
bisect.bisect_left(a, 40)    # 6  → first index with a[i] >= 40

# Binary search on the answer: find the FIRST value where
# a yes/no question flips from False to True.
def first_true(lo, hi, ok):
    while lo < hi:
        mid = (lo + hi) // 2
        if ok(mid):
            hi = mid          # mid might be the answer — keep it
        else:
            lo = mid + 1      # mid is definitely not
    return lo

# smallest k with k*k >= 50
first_true(0, 50, lambda k: k * k >= 50)   # 8  (7*7 = 49)

Same halving, but on a range of possible answers instead of an array: smallest box size that fits, earliest day a build broke, minimum speed to finish on time.

Why logarithms win

items (n)              linear (worst)   binary (worst)
────────────────────────────────────────────────────
100                    100              7
1,000                  1,000            10
1,000,000              1,000,000        20
1,000,000,000          10⁹              30
8 billion (every person) 8 × 10⁹         33

Doubling the data adds one comparison.

The catch is the sort. Sorting costs O(n log n) once. If you search once, just scan: O(n). If you search many times (k searches), sort once and binary search: O(n log n + k log n) beats O(k · n) quickly.

Binary search needs random access (jump to index mid in O(1)). On a linked list, finding the middle costs O(n), so the advantage disappears. Databases get the same effect on disk with B-tree indexes.

Gotchas

Words to know

Binary searchSearching a sorted range by comparing with the middle and discarding the half that can't hold the target.
InvariantA statement kept true before every loop step — here, 'the target, if present, is in a[lo..hi]'.
Insertion pointThe index where a missing value would go to keep the array sorted.
Lower bound (bisect_left)The first index whose value is ≥ x.
Upper bound (bisect_right)The first index whose value is > x.
Monotonic predicateA yes/no test that is false up to some point and true from then on — what 'binary search on the answer' needs.

In two minutes

Test yourself

1. What must be true of the data for binary search to work?

  1. It contains no duplicates
  2. Its length is a power of two
  3. It is sorted by the value you're searching on
  4. It is small enough to fit in memory
Show the answer

It is sorted by the value you're searching on — Discarding a half is only safe because sorted order tells you the target can't be there. Duplicates are fine (the basic version returns one of them) and any length works.

2. Time and extra space of iterative binary search?

  1. O(log n) time, O(1) space
  2. O(n) time, O(1) space
  3. O(log n) time, O(n) space
  4. O(n log n) time, O(1) space
Show the answer

O(log n) time, O(1) space — The range halves each step → log₂ n steps. The loop uses just lo, hi and mid. The recursive version needs O(log n) stack space; O(n log n) is the cost of sorting, not searching.

3. a[mid] < target. What's the correct update?

  1. hi = mid - 1
  2. lo = mid
  3. hi = mid
  4. lo = mid + 1
Show the answer

lo = mid + 1 — mid and everything left of it are too small, and mid has been checked, so the new range starts at mid + 1. lo = mid keeps a checked element and can loop forever when hi = lo + 1.

4. A sorted list holds 1 billion user IDs. Worst-case comparisons for binary search?

  1. About 1,000
  2. About 30
  3. About 500 million
  4. About 9
Show the answer

About 30 — log₂(10⁹) ≈ 29.9, so 30. 'About 9' is log₁₀ — the base is 2 because each step splits in two. 500 million is the average for a linear scan.

5. You must check whether one value is in an unsorted list of 10,000 items, once. Fastest?

  1. Sort it, then binary search
  2. Linear scan
  3. Binary search it as is
  4. Build a balanced BST first
Show the answer

Linear scan — A scan is O(n) = 10,000 steps. Sorting first costs O(n log n) ≈ 130,000 comparisons before you even search. Sorting pays off only when you'll search many times. Binary search on unsorted data gives wrong answers.

6. In Python, which call gives the index where 20 should be inserted into a sorted list a?

  1. a.index(20)
  2. sorted(a).find(20)
  3. heapq.nsmallest(20, a)
  4. bisect.bisect_left(a, 20)
Show the answer

bisect.bisect_left(a, 20) — bisect_left returns the first index with a[i] >= 20 in O(log n). a.index raises ValueError if 20 is missing and scans linearly; lists have no .find.

There are 8 questions, 4 puzzles and 6 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →