Skip to content

Binary search

In a sorted array, comparing with the middle discards half of what is left, so at most ⌊log₂ n⌋ + 1 probes find a key or prove it absent.

Helps to know firstLinear search

Helps to know first
This topic

First, the picture

Guess a number from 1 to 100, told only “higher” or “lower”: start at 50 and every answer halves what is left. Watch the same trick find 100 among 64 sorted numbers.

Halving the search at each step

64 sorted numbers; each probe rules out half of what is left

static int probes = 0; int binary_search(const int *a, int n, int key){    int lo = 0, hi = n - 1;    while (lo <= hi) {        int mid = lo + (hi - lo) / 2;        probes++;        if (a[mid] == key)            return mid;        if (a[mid] < key)            lo = mid + 1;        else            hi = mid - 1;    }    return -1;}int main(void){    int a[N];    for (int i = 0; i < N; i++)        a[i] = 3 * i + 1;    int at = binary_search(a, N, KEY);    printf("key %d: index %d after %d probes\n", KEY, at, probes);    return 0;}

main has started: a has room for 64 ints, none written yet.

probes
0
cells left
–
100

Output

0.00 / 30.50 s
Describe this picture

A strip of the 64 cells of int a[64], wrapped onto rows, with the code of bsearch.c beside it, the line about to run marked. The fill loop runs as one beat and the cells hold 1, 4, 7, … up to 190, three apart. Then binary_search looks for 100. The markers lo and hi sit below the strip at the two ends of the range still searched, and mid above it at the middle. Each probe plays in a few beats: mid lands, the cell is tinted as it is compared with the key, and the half that cannot hold the key greys out. The five probes look at cells 31 (94: go right), 47 (142: go left), 39 (118: left), 35 (106: left) and 33, which holds 100: found, in the second colour. The output reads key 100: index 33 after 5 probes. The readouts give the probes so far and the cells left to search: 64, 32, 15, 7, 3. After the clip, the slider “Key” runs from 0 to 192; a change replays the search at once on the program built for that key. Keys that are not in the array end with lo past hi and index -1, and no key takes more than 7 probes. The transport plays and pauses, steps one line back or forward, and scrubs; the key and a paused step are kept in the page address as search.key and search.step.

Halving what is left

A linear search can rule out only one cell per comparison. If the array is sorted, one comparison with the middle cell rules out half the array: if the middle is smaller than the key, the key can only be to its right; if larger, only to its left. Each probe halves what is left, so even a million cells take only about 20 probes.

This only works because the array is in order. In an unsorted array, a smaller middle says nothing about which half the key is in.

The code and its cost

The loop

int binary_search(const int *a, int n, int key)
{
    int lo = 0, hi = n - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        probes++;
        if (a[mid] == key)
            return mid;
        if (a[mid] < key)
            lo = mid + 1;
        else
            hi = mid - 1;
    }
    return -1;
}

lo and hi are the first and last cells still in the running; mid is the cell halfway between. After a probe the range shrinks to one side of mid, and mid itself is left out, since it has just been checked.

The invariant

The loop keeps one promise, its invariant: if the key is in the array, it lies in a[lo..hi]. It is true at the start, when lo..hi is the whole array, and each probe keeps it true, because the half it drops cannot hold the key. When the range is empty, lo > hi, the key cannot be anywhere, and the function returns -1. The loop always ends: every pass makes the range at least one cell shorter.

How many probes

Each probe either finds the key or leaves at most half the range. A range of n cells can be halved ⌊log₂ n⌋ times before one cell is left, and that last cell takes one more probe, so a search takes at most

⌊log⁡2n⌋+1\lfloor \log_2 n \rfloor + 1

probes, found or not: 7 for 64 cells, 20 for a million, 30 for a billion.

Writing mid

(lo + hi) / 2 looks simpler, but lo + hi can pass the largest int, 2,147,483,647, on a very large array, and signed overflow in C is undefined. lo + (hi - lo) / 2 never adds two large numbers: hi − lo is at most the length of the range.

Worked example

Key 100, by hand.

Probelohimida[mid]Next
1063319494 < 100: lo = 32
2326347142142 > 100: hi = 46
3324639118118 > 100: hi = 38
4323835106106 > 100: hi = 34
5323433100found

Key 101, absent. The first five probes are the same, until (32, 34, 33): 100 < 101, so lo = 34. Then (34, 34, 34): a[34] = 103 > 101, so hi = 33. Now lo is past hi and the search stops after 6 probes with -1. The ends: 190, the last cell, takes 7 probes, at cells 31, 47, 55, 59, 61, 62 and 63; 0, below every cell, takes 6, at 31, 15, 7, 3, 1 and 0, and stops with hi = −1.

Two off-by-one mistakes. Starting with hi = n while the loop tests lo <= hi lets mid reach n, one past the last cell. bsearch-hi-n.c searches 1 4 7 10 13 16 for 20 that way; the sanitiser build stops with stack-buffer-overflow, a read of 4 bytes past the array in binary_search. Reading past the array is undefined behaviour; this plain build read whatever lay there, went on and printed -1.

Writing lo = mid instead of lo = mid + 1 can stop the range from shrinking. In bsearch-lo-mid.c, looking for 16, the last of the six, the range reaches lo = 4, hi = 5; mid is 4 + (5 − 4)/2 = 4, a[4] = 13 is smaller than 16, and lo = mid leaves lo at 4. The same pass repeats for ever; the program stops itself after 6 passes, printing passes 4, 5 and 6 each as lo 4, hi 5, mid 4.

Overflow. With lo = 1,500,000,000 and hi = 1,600,000,000, lo + hi = 3,100,000,000, more than 2,147,483,647. lo + (hi - lo) / 2 gives 1,550,000,000 without ever leaving the range of int. overflow-mid.c prints both.

Recursively. The same search can call itself on the half that is left, one call per probe:

int search(const int *a, int lo, int hi, int key)
{
    if (lo > hi)
        return -1;
    int mid = lo + (hi - lo) / 2;
    if (a[mid] == key)
        return mid;
    if (a[mid] < key)
        return search(a, mid + 1, hi, key);
    return search(a, lo, mid - 1, key);
}

On the 64 cells it goes 5 calls deep for 100 and 7 for 190, never more than the loop’s probes. (The traced recursive.c writes the same function with a depth counter added.)

Two checks. At most how many probes for 1,000 items? (⌊log₂ 1000⌋ + 1 = 10.) Why must the array be sorted? (Otherwise comparing with the middle cannot rule out a half.)

Where it’s used

Sorted tables. Dictionaries, symbol tables and indexes kept in order are searched this way; the C library’s bsearch does it for any array and comparison function.

Finding a bad change. Version-control tools bisect a history of changes: test the middle one, and half the history is cleared at each step.

Solving equations. Finding where a function crosses zero by halving an interval that contains the crossing is the same idea on numbers instead of cells.

Reference card

Code or ideaMeaning
needsa sorted array
lo = 0, hi = n - 1the range still searched
mid = lo + (hi - lo) / 2the middle, without overflow
a[mid] < keylo = mid + 1
a[mid] > keyhi = mid - 1
invariantif present, the key is in a[lo..hi]
absentthe loop ends with lo > hi
probesat most ⌊log₂ n⌋ + 1: 7 for 64, 20 for 10⁶

Platform: C11, gcc on x86-64 Linux: int 4 bytes, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look