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
- –
Output
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
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.
| Probe | lo | hi | mid | a[mid] | Next |
|---|---|---|---|---|---|
| 1 | 0 | 63 | 31 | 94 | 94 < 100: lo = 32 |
| 2 | 32 | 63 | 47 | 142 | 142 > 100: hi = 46 |
| 3 | 32 | 46 | 39 | 118 | 118 > 100: hi = 38 |
| 4 | 32 | 38 | 35 | 106 | 106 > 100: hi = 34 |
| 5 | 32 | 34 | 33 | 100 | found |
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 idea | Meaning |
|---|---|
| needs | a sorted array |
lo = 0, hi = n - 1 | the range still searched |
mid = lo + (hi - lo) / 2 | the middle, without overflow |
a[mid] < key | lo = mid + 1 |
a[mid] > key | hi = mid - 1 |
| invariant | if present, the key is in a[lo..hi] |
| absent | the loop ends with lo > hi |
| probes | at 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.