Binary Search and Interpolation Search
Imagine searching for a name in a phone book containing a million entries. You would never start at page one and scan linearly. Instead, you would flip to the middle, decide whether your name lies earlier or later, and repeat. This everyday intuition is exactly Binary Search, one of the oldest and most consequential ideas in computer science. It turns a task that could take a million steps into one that takes about twenty. It does this by exploiting a single structural fact: the data is sorted. Binary search is the workhorse behind database index lookups. It powers the `bisect` module in Python, `std::lower_bound` in C++, and version-control "bisect" commands. Those commands find the commit that introduced a bug. It also underlies countless library routines that answer "does this sorted collection contain x?" or "where would x be inserted?" Its logarithmic running time means that even for a collection of size , only 64 comparisons are ever needed. This property is so robust that it barely matters whether is a thousand or a trillion. Interpolation search asks a sharper question. Binary search always guesses the middle of the remaining range. But could we guess smarter using the actual values? This is the way a human searches a phone book, jumping proportionally toward where "Smith" should be rather than always splitting in half. When keys are numeric and roughly uniformly distributed, this proportional guess converges dramatically faster. It needs expected comparisons instead of . However, it can degrade to linear time on adversarial or highly skewed data. This article gives a precise, invariant-driven treatment of both algorithms. It provides exact pseudocode with correct boundary handling, the single most bug-prone part of binary search. It also offers a rigorous derivation of the bound. There is an intuitive but honest account of why interpolation search achieves on uniform data. The article also catalogues the classic off-by-one errors that make binary search infamous even among experienced programmers. It closes by connecting these ideas to Linear-Time Selection. There, a different divide-and-conquer strategy achieves linear time on unsorted data. It does so by giving up the ability to binary search at all.
- Binary search finds a target in a sorted array of elements in time by repeatedly halving the search range.
- Its correctness rests on a loop invariant: if the target is present, it always lies within the current window.
- Interpolation search replaces the midpoint guess with a value-proportional guess, achieving expected time on uniformly distributed keys.
- Interpolation search degrades to worst case on adversarial or highly clustered data, unlike binary search's guaranteed .
- Both require Array Indexing with random access in time; neither works efficiently on linked structures.
- Correct implementation demands careful handling of integer overflow, boundary inclusivity, and duplicate-key semantics.
Sorted order turns "where is it?" into a repeatable yes/no question that discards half (or more) of the remaining search space every step.
The idea
A search over an array asks whether a target value appears, and if so, at what index. Without structure, the only guarantee is linear scanning. Sortedness — the invariant that — is a global property that lets a single comparison at one index rule out an entire contiguous region. If , then by transitivity every element at or before is also less than , so the target (if present) must lie strictly after . This single fact is the entire engine of binary search.
Binary search maintains a candidate window that is guaranteed to contain the target if it exists anywhere in the array, and shrinks this window by roughly half at each step by probing the midpoint. Because the window's size decreases geometrically, the number of iterations needed to shrink it to size zero is bounded by , giving the celebrated complexity — a program that finds one item among a billion in about 30 steps.
Interpolation search is a refinement that keeps the same window-shrinking philosophy but changes where it probes. Rather than always bisecting, it uses the numeric values at the endpoints to estimate, via linear interpolation, where "ought to be" if the data were evenly spread — analogous to opening a dictionary near the back when searching for a word starting with "T." When the underlying key distribution really is close to uniform, this guess is so good that the expected number of probes shrinks to , a doubly-logarithmic bound that is asymptotically far smaller than (for , but ). The cost of this speed is fragility: on skewed, clustered, or adversarially chosen data the interpolation estimate can be consistently wrong, and the algorithm degenerates to linear-time, worst-case behavior.
How it works
Binary search operates on a half-open or closed integer index range and maintains two pointers, `lo` and `hi`, representing the current bounds of the search. The key invariant, which must be true before and after every loop iteration, is: > Invariant: if occurs in , then it occurs at some index with . Each iteration computes a midpoint, compares to , and shrinks the window on the side that must still contain — crucially, it must never discard the midpoint itself without checking it, and it must never re-include an index already ruled out (the classic source of infinite loops or missed elements). ``` function BINARY-SEARCH(A, x): // A is sorted ascending, A[1..n] lo <- 1 hi <- n while lo <= hi: mid <- lo + floor((hi - lo) / 2) // avoids overflow of (lo+hi) if A[mid] == x: return mid // found else if A[mid] < x: lo <- mid + 1 // discard A[lo..mid], x is to the right else: hi <- mid - 1 // discard A[mid..hi], x is to the left return NOT-FOUND ``` Two implementation details matter a great deal in practice. First, `mid <- lo + floor((hi - lo) / 2)` is preferred over `floor((lo + hi) / 2)` because the latter can overflow fixed-width integers when `lo` and `hi` are both large — a real bug that shipped in production binary-search implementations for years. Second, the loop condition `lo <= hi` (not `lo < hi`) together with `mid + 1` and `mid - 1` (not `mid` and `mid`) guarantees the window strictly shrinks each iteration, which is what prevents infinite loops. A common variant, sometimes called lower-bound binary search, does not stop early on equality but instead narrows the window until `lo == hi`, returning the smallest index at which could be inserted while keeping the array sorted (useful for finding the first occurrence among duplicates, or for insertion points): ``` function LOWER-BOUND(A, x): lo <- 1 hi <- n + 1 // hi is one past the end; open interval [lo, hi) while lo < hi: mid <- lo + floor((hi - lo) / 2) if A[mid] < x: lo <- mid + 1 else: hi <- mid return lo // first index with A[lo] >= x ```
Interpolation search keeps the same two-pointer skeleton but replaces the blind midpoint with a value-weighted estimate. If the keys were perfectly linear between and , the target's position would satisfy
This is exactly linear interpolation between two known points, hence the name. ``` function INTERPOLATION-SEARCH(A, x): lo <- 1 hi <- n while lo <= hi and x >= A[lo] and x <= A[hi]: if A[hi] == A[lo]: // avoid divide-by-zero if A[lo] == x: return lo else: return NOT-FOUND pos <- lo + floor( (x - A[lo]) * (hi - lo) / (A[hi] - A[lo]) ) if A[pos] == x: return pos else if A[pos] < x: lo <- pos + 1 else: hi <- pos - 1 return NOT-FOUND ``` Note the guard `x >= A[lo] and x <= A[hi]`: since the estimate assumes lies between the endpoint values, the loop must terminate (reporting failure) the moment falls outside the currently bracketed value range, not just the index range. The degenerate case (all remaining keys equal) is handled separately to avoid division by zero.
Complexity & correctness
Binary search — correctness. The proof is a direct application of the loop invariant stated above. Initialization: before the first iteration, and , so the invariant "if is present it lies in " holds trivially since is the whole array. Maintenance: suppose the invariant holds at the start of an iteration and is present at index . If , the algorithm returns the correct index immediately. If , sortedness implies for all , so , meaning — exactly the new window. Symmetrically, if , then , so . In either branch the invariant is preserved for the shrunk window. Termination: the loop runs while . If it exits because , the window is empty, and by the invariant cannot be present anywhere, so `NOT-FOUND` is correct. Since strictly decreases every iteration (either increases past mid or decreases below mid), the loop is guaranteed to terminate.
Binary search — complexity. Let be the number of comparisons on a window of size . Each iteration inspects one element and recurses on a window of size at most (the larger of the two halves), giving the recurrence
By the Master Theorem (or direct unrolling: after halvings the window has size , which reaches when ), this solves to . This holds in the best case (target found immediately, ), average case, and worst case — the bound is essentially tight in all regimes because the window size determines progress deterministically, regardless of where actually is. Space complexity is for the iterative version (two pointers), or for a naive recursive version due to call-stack depth — a good reason to prefer the iterative form in production code.
Interpolation search — correctness. The same invariant argument used for binary search applies verbatim: at every iteration, if is present, it lies in , because the branch taken only discards a region proven (by sortedness) not to contain . Interpolation search is binary search's invariant with a different probe rule — changing where you look inside a range that is known to bracket the answer never breaks correctness, as long as every discarded region is provably excluded. The extra guard checking against and ensures the algorithm reports failure rather than computing a nonsensical or out-of-range `pos` when cannot possibly be present.
Interpolation search — complexity. The worst case is : consider an array where are all and , searching for a value near the low end can force the interpolation estimate to repeatedly land near `hi`, shrinking the window by only one element per probe (much like a bad pivot choice in quicksort). This is the fundamental tradeoff — interpolation search trades a worst-case guarantee for potentially much better average-case performance. Under the assumption that keys are drawn i.i.d. from a distribution close to uniform on a real interval, a classical analysis (Yao & Yao, and independently Perl, Itai, and Avni, 1978) shows the expected number of probes is
The intuition: interpolation search's estimate error shrinks quadratically relative to the window size at each step under uniformity, rather than merely halving. Concretely, if denotes the expected window size after probes, one can show heuristically that rather than . Since squaring the exponent of a logarithm each round, , repeated squaring of the window size itself means the number of rounds needed to shrink down to a constant is only , versus rounds needed when the window merely halves each time. This doubly-exponential shrinkage is the entire reason interpolation search can dramatically outperform binary search on well-behaved numeric data — but it is precisely this reliance on the "linear-interpolation-is-a-good-guess" assumption that makes it fragile on non-uniform inputs.
Worked example
Consider a sorted array of examination scores (values, 1-indexed):
Binary search for (traced in the figure above):
| Step | lo | hi | mid | A[mid] | Comparison | Action |
|---|---|---|---|---|---|---|
| 1 | 1 | 15 | 8 | 33 | 33 < 47 | lo ← 9 |
| 2 | 9 | 15 | 12 | 58 | 58 > 47 | hi ← 11 |
| 3 | 9 | 11 | 10 | 47 | 47 == 47 | return 10 |
Three comparisons versus a hypothetical linear scan that would need ten.
Interpolation search for the same , same array, (), ():
, so . Recompute with (), ():
— found in 2 probes, one fewer than binary search, because the values happen to be nearly evenly spaced and interpolation's linear guess landed close to the true position immediately.
Contrast on skewed data. Suppose instead the array is
Interpolation search computes, with (), ():
, so . The next estimate barely moves either, because the enormous outlier at index 15 flattens the interpolation slope, forcing the window to shrink by roughly one index per probe — degrading toward the worst case despite the array being perfectly sorted. Binary search, unaffected by magnitude, still finds in probes regardless of this skew.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Overflow computing `mid` | `(lo + hi) / 2` overflows when `lo`, `hi` are large signed integers | Use `lo + (hi - lo) / 2` |
| Off-by-one on bounds | Using `mid` instead of `mid ± 1` when shrinking, causing infinite loops | Always exclude the just-checked `mid` from the new window |
| Wrong loop condition | `lo < hi` skips the case where the window has exactly one element | Use `lo <= hi` for closed intervals; be consistent with open/closed semantics |
| Assuming sortedness | Running binary search on an unsorted or partially-sorted array | Verify precondition; binary search gives no error signal for violated invariants |
| Division by zero in interpolation | `A[hi] == A[lo]` when all remaining keys are equal | Special-case equal endpoints before computing `pos` |
| Ignoring interpolation's worst case | Assuming always holds regardless of data distribution | Fall back to binary search (or a hybrid) when data may be adversarial |
| Duplicate keys ambiguity | Binary search may return any matching index, not the first or last | Use explicit lower-bound/upper-bound variants when the specific occurrence matters |
Pioneers & history
Binary search's core idea — bisecting an ordered range — predates modern computing, but its rigorous algorithmic treatment and the notorious subtlety of implementing it correctly were documented in depth by Donald Knuth, who famously noted that although the basic idea was published as early as 1946, the first fully correct published version did not appear until 1962, and many "textbook" implementations in between contained bugs. Interpolation search was formalized and analyzed rigorously in the late 1970s, with the expected bound established through independent probabilistic analyses.
Origins · 1 minDonald E. Knuth and how this was discovered
Knuth's meticulous analysis of binary search — cataloguing decades of subtly incorrect published implementations — became a foundational case study in the importance of loop invariants and formal correctness proofs for even the simplest-looking algorithms.
Where this matters
Binary search underlies `std::lower_bound`/`std::upper_bound` in C++, the `bisect` module in Python, `Arrays.binarySearch` in Java, and countless database B-tree node lookups where sorted keys within a node are searched before descending. Version control systems expose `git bisect`, a direct application of the binary-search invariant to find the commit that introduced a regression by testing the "midpoint" commit and discarding half the history each round. Compilers and interpreters use sorted symbol tables searched via binary search for fast identifier resolution. Interpolation search finds use in specialized settings — indexing large numeric datasets with roughly uniform key distributions, such as certain database indexes on auto-incrementing IDs or timestamps — though many production systems favor binary search or hybrid schemes (falling back to binary search when interpolation estimates misbehave) precisely because of interpolation search's brittle worst case. The same divide-and-shrink philosophy that powers binary search reappears, transformed, in Linear-Time Selection, where a clever pivot-choosing strategy achieves worst-case linear time on unsorted arrays by discarding a guaranteed fraction of elements each round — an idea deeply related to, yet fundamentally different from, the sorted-array assumption that makes binary search possible. Both algorithms also rely on Array Indexing providing random access — a precondition that fails for linked lists, where binary search's advantage disappears entirely.
Ask the tutor: How does the choice between binary search's guaranteed Θ(log n) and interpolation search's expected Θ(log log n) change if the keys are integers with many repeated values rather than continuous real numbers?
Does binary search work on unsorted arrays?
No. Binary search's entire correctness argument depends on sortedness: discarding a half of the array based on a single comparison is only valid because sortedness guarantees every element on the excluded side compares the same way against the target. On an unsorted array, tells you nothing about the elements before or after `mid`, so the algorithm may discard the region actually containing the target. Running binary search on unsorted data produces no error — it just silently returns wrong or missing results, which is a particularly dangerous failure mode. If the array is unsorted, either sort it first (paying once, amortized over many searches) or use a linear scan.
Why does interpolation search sometimes perform worse than binary search in practice, despite the better asymptotic bound?
The bound is an expected-case guarantee under a uniformity assumption, not a worst-case guarantee. Real-world numeric data is often clustered, has outliers, or follows skewed distributions (e.g., power laws, exponential timestamps), which can make the interpolation estimate consistently far from the true position. In addition, each interpolation-search iteration performs extra arithmetic (subtraction, multiplication, division) compared to binary search's simple bit-shift-like midpoint computation, so on small or moderately sized arrays the constant-factor overhead can outweigh the asymptotic advantage. In practice, interpolation search is worthwhile mainly for very large arrays of well-behaved numeric keys, and many systems use a hybrid that falls back to binary search when interpolation estimates seem to be misbehaving.
How does binary search handle duplicate keys?
The basic `BINARY-SEARCH` pseudocode returns some index where , but if there are duplicates, it makes no guarantee about which occurrence is returned — different valid execution paths can return different matching indices. When the first or last occurrence matters (e.g., counting occurrences, or finding an insertion range), use the lower-bound and upper-bound variants, which never stop early on equality and instead continue narrowing until the window collapses to a single boundary index. `LOWER-BOUND` returns the first index with ; a symmetric `UPPER-BOUND` returns the first index with , and the pair gives the entire range of matching duplicates in total.
Can interpolation search be combined with binary search for robustness?
Yes — this is a common engineering pattern. One approach caps the number of consecutive "bad" interpolation probes (where the window shrinks by only a small fraction) and switches to a standard binary-search midpoint whenever the interpolation estimate seems to be making insufficient progress, guaranteeing an overall worst case while retaining the potential expected-case speedup on well-behaved data. This hybrid strategy mirrors a broader pattern in algorithm design — such as introsort combining quicksort's average-case speed with heapsort's worst-case guarantee — where a fast but fragile method is paired with a slower but bulletproof fallback.
Why is the loop invariant approach important for proving binary search correct, rather than just testing it?
Testing can show an algorithm works on the cases tried, but binary search has notoriously many boundary conditions (empty array, single element, target at either end, target absent, duplicate keys) that are easy to miss and where subtle bugs hide. A loop invariant gives a mathematical guarantee that holds for every possible input satisfying the precondition (sortedness), proven once via induction rather than checked case by case. This is precisely why Knuth's historical survey found that many published, seemingly-tested binary search implementations were nonetheless incorrect: intuition and testing missed edge cases that a rigorous invariant-based proof would have caught immediately.
What is the space complexity of binary search, and does recursion change it?
The iterative version shown above uses only two integer variables (`lo`, `hi`), giving auxiliary space beyond the input array. A recursive implementation that calls itself on the shrunk range instead uses space, because each recursive call adds a stack frame, and the recursion depth equals the number of halvings, . Many languages optimize tail recursion into iteration, but this is not guaranteed across all compilers or runtimes, so performance-critical code typically prefers the explicit iterative form to guarantee constant auxiliary space.
Does interpolation search require the array to be sorted, just like binary search?
Yes — sortedness is a precondition for both algorithms, since both rely on the same core invariant that a comparison at one index rules out a contiguous region. Interpolation search additionally implicitly assumes something stronger: that the numeric spacing of keys is informative, i.e., that a value roughly a third of the way between and is likely to be located roughly a third of the way through the index range. Binary search makes no such distributional assumption — it works identically well (in terms of asymptotic comparisons) regardless of how the values are spaced, which is exactly why it offers a worst-case guarantee that interpolation search cannot match.