computer-science/binary-search-how-to-find-one-number-in-a-million-with-20-guesses.md

Binary search: how to find one number in a million with 20 guesses

Computer science · 4 min read ·

Guessing a number between 1 and 1,000,000 sounds like it could take a million tries. With binary search it takes at most 20.

Step through a sorted list of 64 numbers, tap any target, and watch each guess rule out half of what is left. Then read the short Python function that does the same thing, see real output on a million items, and learn why the list must be sorted first and where beginners get stuck.

Row of sixteen boxes with one highlighted in the middle and the outer boxes faded, showing half the list ruled out

Imagine a friend thinks of a number from 1 to 1,000,000 and says only "higher" or "lower" after each guess. Guessing 1, then 2, then 3 could take a million tries. Guessing smartly takes at most 20. The smart way is called binary search, and it is one of the most useful ideas in programming.

The idea: throw away half every time

Start by guessing the number in the middle. If your friend says "higher", you now know the answer is in the top half, so the whole bottom half is gone. Guess the middle of what is left, and repeat. Each guess cuts the remaining possibilities in half.

There is one requirement: the items must already be sorted (smallest to largest, or A to Z). Without order, "higher" and "lower" would tell you nothing about where to look next.

Try it: 64 sorted numbers

Tap any number to make it the target (it gets a dashed orange outline). Then press Step to make one guess. The bright cell is the guess, bright-bordered cells are still possible, and faded cells have been ruled out.

Try the first number, then the last, then one in the middle. Notice that a target near the left edge costs a plain scan almost nothing, but binary search never needs more than 7 guesses here, whichever number you pick.

The same thing in Python

Keep two markers, low and high, around the part of the list that could still hold the answer. Each loop looks at the middle one:

def binary_search(items, target):
    low, high = 0, len(items) - 1
    guesses = 0
    while low <= high:
        mid = (low + high) // 2      # // rounds down to a whole number
        guesses += 1
        if items[mid] == target:
            return mid, guesses      # found it: position and guess count
        elif items[mid] < target:
            low = mid + 1            # target is in the upper half
        else:
            high = mid - 1           # target is in the lower half
    return -1, guesses               # -1 means "not in the list"

Running it on a sorted list of every number from 1 to 1,000,000 gives these real results:

numbers = list(range(1, 1_000_001))
print(binary_search(numbers, 737_421))    # (737420, 20)
print(binary_search(numbers, 1))          # (0, 19)
print(binary_search(numbers, 2_000_000))  # (-1, 20)

The first number is the position (counting from 0) and the second is how many guesses it took. Finding 737,421 in a million numbers took 20 guesses. A number that is not in the list at all took 20 guesses to rule out, instead of a million.

Why 20?

Ask the question backwards: how many times can you halve a million before you reach 1? Halving 1,000,000 repeatedly goes 500,000, 250,000, 125,000 and so on, and it takes about 20 halvings. Each extra guess doubles the size of list you can handle:

Items in the listMost guesses needed
647
1,00010
1,000,00020
1,000,000,00030

A billion items need only 30 guesses. Programmers write this growth as O(log n), read as "logarithmic": the work grows very slowly as the data grows. A plain scan from the left is O(n): a thousand times more data means a thousand times more work.

Where beginners trip up

The list must be sorted. Run the function on the unsorted list [5, 1, 9, 3] and searching for 5 returns -1 ("not found") even though 5 is the first item, with no error message.

Sorting costs something too. If you only search a list once, a plain scan is fine. Binary search pays off when you search the same sorted data many times, which is what a phone's contact list or a database index does.

Off-by-one mistakes. Writing low = mid instead of low = mid + 1 can make the loop repeat forever, because the middle guess is already known to be wrong and must be excluded.

Use the built-in version

In real code you rarely write this by hand. Python's standard library has a bisect module that does the halving for you:

from bisect import bisect_left
print(bisect_left([2, 4, 6, 8, 10], 6))   # 2

It returns the position where the value is, or where it would be inserted to keep the list sorted. Check that the item at that position equals your target before treating it as found. The idea underneath is the same one you just stepped through: halve, compare, repeat.

$ ls computer-science/

see all →
computer-science/merge-sort-how-splitting-a-list-in-half-again-and-again-sorts-it-with.md Merge sort: how splitting a list in half again and again sorts it with far less work How do you sort 10,000 numbers without making millions of comparisons? Merge sort never sorts a big pile. It splits the list until every… 5 min read · 27 Oct computer-science/hash-tables-how-a-computer-finds-one-name-among-thousands-without-chec.md Hash tables: how a computer finds one name among thousands without checking them all Your phone finds one contact among thousands without reading them all. The trick is a hash table: a row of numbered slots and a small… 6 min read · 24 Oct computer-science/stacks-and-queues-why-undo-takes-the-newest-item-and-a-printer-takes-t.md Stacks and queues: why undo takes the newest item and a printer takes the oldest Press undo and the last thing you typed disappears first. Send documents to a shared printer and the first one is printed first. Both are… 5 min read · 20 Oct computer-science/breadth-first-search-how-a-maze-solver-finds-the-shortest-way-out.md Breadth-first search: how a maze solver finds the shortest way out How does a maze solver, or a map app, find the shortest route and not just any route? The answer is breadth-first search: explore every… 6 min read · 17 Oct computer-science/big-o-how-to-tell-if-your-code-will-survive-1-000-times-more-data.md Big O: how to tell if your code will survive 1,000 times more data Your code works on 10 items, then crawls on 10,000. Big O is the short way programmers describe how the number of steps grows as the input… 4 min read · 11 Oct computer-science/recursion-why-fib-30-makes-2-7-million-calls-and-how-a-notebook-fixes.md Recursion: why fib(30) makes 2.7 million calls, and how a notebook fixes it A function that calls itself sounds like an endless loop, but it works when every call gets a smaller problem and one case is simple enough… 5 min read · 8 Oct