The C Path — learn C, visually

📈 Algorithms & Complexity

Searching: linear vs binary

⏱ 13 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Play "guess my number between 1 and a million" the smart way — always guess the middle — and you win in just twenty questions. That exact trick, written in C, is how your phone finds a contact instantly and how a database finds a row without reading the whole table.

You'll build it yourself — and meet the sneaky one-line bug in it that hid inside Java's official library for nine years.

You have an array of n values and you want to know: is 23 in there, and where? This tiny problem is the perfect first arena for last lesson's Big-O ideas, because two correct solutions to it differ so dramatically that one takes a million steps where the other takes twenty.

Linear search: the honest baseline

No assumptions, no tricks: walk the array front to back and compare. It works on any array, sorted or not, and it's the best you can possibly do when the data is unordered — the target could hide anywhere, so you must be prepared to look everywhere.

This spot has an interactive arrayviz widget — open the interactive lesson to play with it.

linear.c
#include <stdio.h>

int linear_search(const int *a, int n, int target) {
    for (int i = 0; i < n; i++)
        if (a[i] == target)
            return i;         /* index of the first hit      */
    return -1;                /* checked all n — not here    */
}

int main(void) {
    int a[] = {14, 3, 92, 41, 7, 66, 25, 58};
    printf("7   is at index %d\n", linear_search(a, 8, 7));
    printf("100 is at index %d\n", linear_search(a, 8, 100));
    return 0;
}
terminal
$ gcc linear.c -o linear && ./linear
7   is at index 4
100 is at index -1

Worst case (target absent or last): n comparisons — Θ(n). Average for a present target: about n/2, which is still Θ(n) after dropping the ½.

🧠 Checkpoint: When is linear search the RIGHT choice, not just the lazy one?

  • Never — binary search is always better
  • When the array is unsorted (and searched once)
  • Only for arrays under 10 elements
  • When the target is definitely present
Show answer

When the array is unsorted (and searched once) — Binary search requires sorted data. Sorting first costs O(n log n) — pointless if you only search once. For unsorted, search-once data, Θ(n) linear scan is optimal. (If you search the same data many times, sorting first pays for itself.)

Binary search: halve or die

Now add one precondition — the array is sorted — and everything changes. Check the middle element. Too small? The target can only be in the right half. Too big? Left half. Either way, one comparison destroys half the remaining candidates. Watch the gray (eliminated) region grow:

This spot has an interactive arrayviz widget — open the interactive lesson to play with it.

binary.c
#include <stdio.h>

int binary_search(const int *a, int n, int target) {
    int lo = 0, hi = n - 1;         /* inclusive bounds       */
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;   /* overflow-safe!     */
        if (a[mid] == target) return mid;
        if (a[mid] < target)  lo = mid + 1;  /* discard left  */
        else                  hi = mid - 1;  /* discard right */
    }
    return -1;
}

int main(void) {
    int a[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
    printf("23 is at index %d\n", binary_search(a, 10, 23));
    printf("7  is at index %d\n", binary_search(a, 10, 7));
    return 0;
}
terminal
$ gcc binary.c -o binary && ./binary
23 is at index 5
7  is at index -1
💀

The famous overflow bug: the "obvious" midpoint mid = (lo + hi) / 2 is broken! If lo and hi are both around a billion, their sum overflows int — undefined behavior. This exact bug sat in Java's official library binary search for nine years, and in Programming Pearls for twenty. The fix is pure algebra: mid = lo + (hi - lo) / 2 computes the same midpoint but the intermediate value never exceeds hi. Type it this way forever, even when "n is small" — habits outlive assumptions.

🧠 Checkpoint: Why write mid = lo + (hi - lo) / 2 instead of (lo + hi) / 2?

  • It compiles to faster code
  • It rounds toward the target
  • The sum lo + hi can overflow int — this form can’t
  • Pure style — they are identical
Show answer

The sum lo + hi can overflow int — this form can’t — With lo and hi near INT_MAX, lo + hi overflows — signed overflow is undefined behavior. hi − lo always fits, so the safe form computes the identical midpoint without the landmine. This bug hid in Java’s standard library for nearly a decade.

Watching the bounds — where the bugs live

Binary search is famously easy to get almost right. Studies found most programmers' first attempts had off-by-one bugs: < vs <= in the loop condition, or forgetting the +1/−1 when shrinking. Step through a correct one and watch lo and hi pincer the target:

This spot has an interactive trace widget — open the interactive lesson to play with it.

⚠️

The invariant that keeps you sane: with inclusive bounds, "if the target exists, it is in a[lo..hi]". That forces all three details: loop while lo <= hi (a one-element range is still live), and move to mid + 1 / mid − 1 (mid itself was just ruled out). Change any one of them and you get infinite loops or missed elements.

n vs log n — the payoff

Halving means the number of comparisons is log₂ n. A million elements? log₂(1,000,000) ≈ 20. A billion? 30. Doubling the data adds one comparison:

This spot has an interactive bigo widget — open the interactive lesson to play with it.

🧠 Checkpoint: Binary search over 1,000,000 sorted elements needs at most about how many comparisons?

  • 20
  • 1,000
  • 500,000
  • 10,000
Show answer

20 — log₂(1,000,000) ≈ 19.9, so 20 halvings reduce a million candidates to one. Each doubling of n adds just ONE comparison — 2 million needs 21.

Don't write it — call it: bsearch

The C standard library ships a generic binary search in <stdlib.h>. Like qsort, it works on any element type via a comparison callback:

use_bsearch.c
#include <stdio.h>
#include <stdlib.h>

int cmp_int(const void *pa, const void *pb) {
    int a = *(const int *)pa, b = *(const int *)pb;
    return (a > b) - (a < b);   /* -1, 0, +1 */
}

int main(void) {
    int a[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
    int key = 23;

    int *hit = bsearch(&key, a, 10, sizeof a[0], cmp_int);
    if (hit) printf("found %d at index %td\n", *hit, hit - a);
    else     printf("not found\n");
    return 0;
}
terminal
$ gcc use_bsearch.c -o use_bsearch && ./use_bsearch
found 23 at index 5
# bsearch returns a POINTER to the element (or NULL);
# subtract the array base to recover the index.

This spot has an interactive editor widget — open the interactive lesson to play with it.

Binary search demands sorted data — so the obvious next question is: how does data get sorted, and what does that cost?

▶ Practice this lesson interactively (with live gcc)