The Needle in the Haystack

You have a sorted array of 12 numbers: [2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35]. Somewhere in there is the number 26.

How would you find it? The obvious way: start at index 0 and check each element. If arr[0] is not 26, try arr[1]. Then arr[2]. Simple, reliable, slow.

In the worst case — target at the end, or not present at all — you check every single element. For 12 elements, that is tolerable. For a million? That is a million comparisons, every time. A database with a billion records would need a billion comparisons in the worst case. At 10 nanoseconds per comparison, that is 10 seconds — for a single lookup.

Drag the slider and feel the gap widen:

n =10
Linear10 checks ~ 100ns
Binary4 checks ~ 40ns

The array is sorted. That is a massive piece of information that linear search completely ignores. When you check arr[5] = 17 and your target is 26, you know that every element before index 5 is also too small. You did not just eliminate one candidate — you eliminated six. Linear search throws away that information and checks the next element anyway.

Try it. Find the target by tapping elements one at a time. Feel how many taps it takes when you cannot skip ahead.

Build, Test, Compare

Linear search works, but it does not scale. In a million-element array, you might need a million taps.

There is a template — a specific arrangement of conditions and updates — that guarantees you find the target in at most 20 checks for a million elements. That is log2(1,000,000) ≈ 20. Each check eliminates half the remaining candidates.

But the template has to be exactly right. One wrong condition and the search silently skips the answer. One wrong update and the loop runs forever. The arrangement of pieces matters more than any individual line.

Assemble it from parts. Then test it on two cases — one where the target exists and one where it does not. Then see what makes it different from the other binary search template.

Find the target
1 / 4
Find: 26
0 clicks
Tap elements to search for the target. One at a time.

The interaction above gives you code tiles from two different templates. Three of those tiles are decoys from the boundary-search template — they look almost identical to the correct tiles but will produce a subtly different search behavior. Placing the right tiles in the right order is the whole challenge.

After assembly, you will step through two test cases to see the template in action. Watch how the search space halves at each iteration:

1
// Test 1: target = 19 in [3, 7, 12, 19, 23, 31]
2
// Iteration 1: mid=2, arr[2]=12 < 19 → lo = 3
3
// Iteration 2: mid=4, arr[4]=23 > 19 → hi = 3
4
// Iteration 3: mid=3, arr[3]=19 === 19 → found!
5
6
// Test 2: target = 9 in [2, 5, 8, 11, 14]
7
// Iteration 1: mid=2, arr[2]=8 < 9 → lo = 3
8
// Iteration 2: mid=3, arr[3]=11 > 9 → hi = 2
9
// lo=3 > hi=2 → loop exits, return -1

Three comparisons to find an element in a 6-element array. Two comparisons to confirm absence in a 5-element array. That is the power of halving.

Two Templates, One Idea

You just built the exact-match template and compared it to the boundary-search template. Three differences, one principle:

Loop condition: lo <= hi vs lo < hi. Exact-match checks the single-element interval because that element might be the answer. Boundary search stops when it converges — no final check needed, because lo already points to the answer.

Update rule: hi = mid - 1 vs hi = mid. Exact-match excludes mid on both branches because mid was already checked and rejected — it is either the target (return immediately) or not (safe to skip). Boundary search keeps mid as a candidate on one side because mid might be the first or last occurrence.

Return value: -1 vs lo. Exact-match answers “is X here?” — the answer might be no. Boundary search answers “where does the property change?” — that always has an answer (even if it is the past-the-end index).

Here are the two templates side by side:

1
// EXACT-MATCH: "Is X here? Where?"
2
let lo = 0, hi = arr.length - 1;
3
while (lo <= hi) {
4
  const mid = lo + Math.floor((hi - lo) / 2);
5
  if (arr[mid] === target) return mid;
6
  if (arr[mid] < target) lo = mid + 1;
7
  else hi = mid - 1;
8
}
9
return -1;
10
11
// BOUNDARY: "Where does F→T flip?"
12
let lo = 0, hi = arr.length;
13
while (lo < hi) {
14
  const mid = lo + Math.floor((hi - lo) / 2);
15
  if (arr[mid] < target) lo = mid + 1;
16
  else hi = mid;
17
}
18
return lo;

The most common binary search bug is using pieces from one template in the other. hi = mid with lo <= hi causes infinite loops — the exit condition and the update rule disagree about when to stop. hi = mid - 1 with lo < hi skips candidates — you exclude mid from consideration on the exact iteration where mid might be the answer.

Here is a concrete example of the danger. Suppose you are solving LC 34 (Find First and Last Position) and you write the boundary template but accidentally use hi = mid - 1 instead of hi = mid:

1
// BUGGY: boundary template with exact-match update
2
let lo = 0, hi = arr.length;
3
while (lo < hi) {
4
  const mid = lo + Math.floor((hi - lo) / 2);
5
  if (arr[mid] < target) lo = mid + 1;
6
  else hi = mid - 1;  // BUG: skips mid, which might be the first occurrence
7
}
8
return lo;

On [2, 2, 2, 3] with target 2: mid = 2, arr[2] = 2 >= target, so hi = mid - 1 = 1. The first 2 is at index 0, but the search already excluded index 2 — which was a valid candidate. The search returns 1 instead of 0. A one-character difference in the update rule, a completely wrong answer.

When you sit down with a binary search problem, the first question is always: “Am I looking for a specific value, or a boundary?” LC 704 (Binary Search) is exact-match. LC 35 (Search Insert Position) is boundary. LC 34 is boundary. LC 875 (Koko Eating Bananas) is boundary on the answer space. The answer to that one question tells you which template to reach for. Everything else follows.