Pattern 5 of 24 · Search & optimisation · 30 min

Binary Search

Halve the search space every step, on a sorted array or on the answer itself, for O(log n) decisions.

Spot it:sorted or rotated arrayfind the first / last positionminimum value that worksO(log n) requiredJavaScript you'll use: Numbers, Math and integer tricks Arrays: create, read, update
Say it in 30 seconds
  • O(log n)Checks; about 20 for a million
  • O(1)Extra space, iterative
  • O(n log R)Searching an answer range R, O(n) per check
Show what to say

“When the input is sorted or rotated, or the answer is the smallest value that makes a yes/no check pass, I binary search: I keep a range that must contain the answer and halve it every step. I use the first-true template, which avoids most off-by-one bugs.”

Trade-offs

  • Exact match vs first true: exact match stops at a hit; first true also finds bounds and insert positions, and ends on the answer.
  • Binary search vs trying every value: O(n log R) against a linear sweep that is hopeless when the answer can reach 10^9.
  • Distinct vs repeated values in a rotated array: O(log n) against O(n) worst case, because equal lo, mid and hi hide which half is sorted.

Bring it up before they ask

Never write lo = mid with a rounded-down mid, start hi at n when past-the-end is a valid answer, and trace 0, 1 and 2 candidates by hand.

0 of 0 sections ticked
30-second answer

Binary search keeps a range that must contain the answer and halves it with every check: about 20 checks for a million candidates. The first true template prevents most off-by-one bugs.

When to reach for it

Spot it in the problem0 of 4

Tap the signals you can see in the problem.

RemembersortedrotatedfirstlastMinimumminimize the maximumO(log n)

Pick your variant
  • Exact match

    Search rangeindices [0, n - 1]

    binary search
  • First true (lower bound)

    Search rangeindices [0, n]

    insert position
  • Rotated array

    Search rangeindices [0, n - 1]

    search rotated
  • On the answer

    Search rangesmallest to largest answer

    Kokoship capacity

The core idea: a range that must contain the answer

Guess a number from 1 to 100 and hear "higher" or "lower". Each answer rules out half the numbers.

Read more · 86 words

One sentence holds every binary search together, the invariant: if the answer exists, it is inside [lo, hi]. Each check proves one half can't hold it, so that half goes.

On a sorted array, if nums[mid] is smaller than the target, everything left of it is too. The left half goes.

Halving is the speed: 10 checks for a thousand, 20 for a million, 30 for a billion. mid rounds down, so lo <= mid < hi while lo < hi. Try your own array with Edit input:

Exact match: halve the range until the target is found

nums (8)
3
8
12
17
21
30
44
52
▲ lo
▲ hi
target
44
lo
0
hi
7

The array is sorted. If 44 is anywhere, it is inside [lo, hi] = [0, 7]: that sentence is the invariant every step must keep true.

function binarySearch(nums, target) {
  let lo = 0, hi = nums.length - 1;
  while (lo <= hi) {
    const mid = Math.floor((lo + hi) / 2);
    if (nums[mid] === target) return mid;
    if (nums[mid] < target) lo = mid + 1;
    else hi = mid - 1;
  }
  return -1;
}
All steps as text
  1. The array is sorted. If 44 is anywhere, it is inside [lo, hi] = [0, 7]: that sentence is the invariant every step must keep true.
  2. lo <= hi, so the range is not empty yet: 8 candidates, indices 0 to 7.
  3. The middle of [0, 7] is index 3, rounding down. Compare nums[3] = 17 with 44.
  4. 17 < 44. The array is sorted, so index 3 and everything left of it are too small. Drop them: lo = 4.
  5. lo <= hi, so the range is not empty yet: 4 candidates, indices 4 to 7.
  6. The middle of [4, 7] is index 5, rounding down. Compare nums[5] = 30 with 44.
  7. 30 < 44. The array is sorted, so index 5 and everything left of it are too small. Drop them: lo = 6.
  8. lo <= hi, so the range is not empty yet: 2 candidates, indices 6 to 7.
  9. The middle of [6, 7] is index 6, rounding down. Compare nums[6] = 44 with 44.
  10. nums[6] is 44: return 6. Binary search looked at 3 elements; a left-to-right scan would have looked at 7.

Play with it

Guess the secret number from hints, then press Halve: it never needs more than 7 guesses for 1–100.

Read more · 110 words

Switch to First bad build to feel the first-true template.

The recipe:

  1. Name the search space

    Indices (up to n if "insert at the end" is valid), or answer values.

  2. Write the yes/no question and check it is monotonic

    ok(x) must be false, then true: like nums[i] >= target. If ok(x) doesn't imply ok(x + 1), it's the wrong tool.

  3. Pick the template and state the invariant

    First true: "everything before lo is false, everything from hi on is true".

  4. Update so the range shrinks

    Never lo = mid with a rounded-down mid: it loops forever.

  5. Run the tiny cases by hand

    Trace 0, 1 and 2 candidates.

Interactive

Halving lab

Can you beat halving? Each middle check throws away half of what is left.

100 of 100 still possiblehalving tries 501100lomidhi
Your checks: 0Halving, worst case: 7Linear scan from 1: up to 100

I picked a number from 1 to 100. Type a guess, click the bar, or press Halve to guess the middle.

Templates

Template 1: exact match

You want one value and can stop when you see it. The range is inclusive, so loop while lo <= hi.

This is Binary SearchEasy line for line.

// Template 1: exact match on a sorted array.
// Invariant: if target is anywhere in nums, it is inside nums[lo..hi] (both ends included).
export function binarySearch(nums, target) {
  let lo = 0;
  let hi = nums.length - 1;
  while (lo <= hi) {                       // the range is not empty
    const mid = Math.floor((lo + hi) / 2); // rounds down; exact for any index in JS
    if (nums[mid] === target) return mid;
    if (nums[mid] < target) lo = mid + 1;  // mid and everything left of it are too small
    else hi = mid - 1;                     // mid and everything right of it are too big
  }
  return -1;                               // empty range: target is not in nums
}

Template 2: first true (lower bound)

Most questions aren't "is it here?"

Read more · 108 words

but "where does it start?": the first copy, the insert position, the first bad version. Picture a yes/no test over the candidates:

hi starts at n, one past the end, because "nothing passes" is a legal answer.

lowerBound answers Search Insert PositionEasy. For Find First and Last Position of a ValueMedium, the first copy is at lowerBound(x) and the last at upperBound(x) - 1.

Off-by-one, settled. The templates come in matched sets:

TemplateLoopUpdatesEnds with
Exact matchlo <= hilo = mid + 1, hi = mid - 1lo === hi + 1 (empty)
First truelo < hilo = mid + 1, hi = midlo === hi (the answer)

Mix the parts and lo <= hi with hi = mid loops forever.

index:          0  1  2  3  4  5  6     (7 = "none")
nums:           1  3  3  3  5  8  9
nums[i] >= 3?   F  T  T  T  T  T  T
                   ^ first true = 1

First true: the first index with nums[i] >= target

nums (7)
1
3
3
3
5
8
9
nums[i] >= 3? (7)
?
?
?
?
?
?
?
▲ lo
▲ hi
target
3
lo
0
hi
7

Ask "is nums[i] >= 3?" for every i. In a sorted array the answers go F, F, …, T, T, and we want the first T. It may not exist, so hi starts at 7, one past the end.

function lowerBound(nums, target) {
  let lo = 0, hi = nums.length;
  while (lo < hi) {
    const mid = Math.floor((lo + hi) / 2);
    if (nums[mid] < target) lo = mid + 1;
    else hi = mid;
  }
  return lo;
}
All steps as text
  1. Ask "is nums[i] >= 3?" for every i. In a sorted array the answers go F, F, …, T, T, and we want the first T. It may not exist, so hi starts at 7, one past the end.
  2. The answer is somewhere in [0, 7]. Check the middle: mid = 3, nums[3] = 3.
  3. 3 >= 3: T. Index 3 might be the first T, so keep it: hi = mid = 3, not mid - 1.
  4. The answer is somewhere in [0, 3]. Check the middle: mid = 1, nums[1] = 3.
  5. 3 >= 3: T. Index 1 might be the first T, so keep it: hi = mid = 1, not mid - 1.
  6. The answer is somewhere in [0, 1]. Check the middle: mid = 0, nums[0] = 1.
  7. 1 < 3: F. Every index up to 0 is F too (the array is sorted), so the first T is after mid: lo = 1.
  8. lo === hi === 1: one candidate is left, so it is the answer. Index 1 is the first value >= 3, the first copy of 3.
// Template 2: first true. ok(i) must be false for a prefix of [lo, hi) and true for the rest.
// Returns the first i with ok(i), or hi if there is none. Invariant: the answer is always in [lo, hi].
export function firstTrue(lo, hi, ok) {
  while (lo < hi) {
    const mid = Math.floor((lo + hi) / 2); // lo <= mid < hi, so both updates shrink the range
    if (ok(mid)) hi = mid;                 // mid might be the first true: keep it
    else lo = mid + 1;                     // mid is false, and so is everything before it
  }
  return lo;                               // lo === hi
}

// First index with nums[i] >= target, or nums.length: the first copy of target, or where it would be inserted.
export function lowerBound(nums, target) {
  return firstTrue(0, nums.length, (i) => nums[i] >= target);
}

// First index with nums[i] > target: one past the last copy of target.
export function upperBound(nums, target) {
  return firstTrue(0, nums.length, (i) => nums[i] > target);
}

// How many values lie in [low, high]? Two bounds, O(log n), no scanning.
export function countInRange(nums, low, high) {
  return Math.max(0, upperBound(nums, high) - lowerBound(nums, low));
}

Template 3: rotated sorted arrays

Rotate [1, 2, 4, 5, 7, 9] into [5, 7, 9, 1, 2, 4] and plain halving breaks.

Read more · 26 words

But cut anywhere and at least one half is still sorted. Its two ends tell you whether the target is inside.

Search in a Rotated Sorted ArrayMedium uses the sorted-half test.

// Template 3: a sorted array of distinct values, rotated. Returns the index of the minimum (where the rotation starts).
// Compare with nums[hi]: if nums[mid] > nums[hi], the drop, and so the minimum, is to the right of mid.
export function findMinIndex(nums) {
  let lo = 0;
  let hi = nums.length - 1;
  while (lo < hi) {
    const mid = Math.floor((lo + hi) / 2);
    if (nums[mid] > nums[hi]) lo = mid + 1; // mid is in the upper run: the minimum comes after it
    else hi = mid;                          // mid is in the lower run: the minimum is mid or before it
  }
  return lo;
}

Template 4: binary search on the answer

Nothing is sorted, but the answer is a number in a known range, and checking one candidate is cheap.

Read more · 62 words

Can every package ship in days days with capacity cap?

If a candidate works, every bigger one does too. So "does x work?" goes no, no, yes, yes, and first true finds the smallest that works.

Search from the smallest value that could work (the heaviest package) to one that surely does (the total). "Minimize the maximum" is the giveaway. Try Koko Eating BananasMedium.

On the answer: the smallest ship capacity that works

capacity (9)
4
5
6
7
8
9
10
11
12
▲ lo
▲ hi
weights (5)
4
2
3
1
2
days
3
lo
4
hi
12

A capacity below the heaviest package (4) can never work, and 12 (everything at once) always does. So the answer is in [4, 12]: search those values, not array indices.

function shipWithinDays(weights, days) {
  let lo = Math.max(...weights);
  let hi = weights.reduce((a, b) => a + b, 0);
  while (lo < hi) {
    const mid = Math.floor((lo + hi) / 2);
    if (daysNeeded(weights, mid) <= days) {
      hi = mid;
    } else {
      lo = mid + 1;
    }
  }
  return lo;
}

function daysNeeded(weights, cap) {
  let used = 1, load = 0;
  for (const w of weights) {
    if (load + w > cap) { used++; load = 0; }  // start a new day
    load += w;
  }
  return used;
}
All steps as text
  1. A capacity below the heaviest package (4) can never work, and 12 (everything at once) always does. So the answer is in [4, 12]: search those values, not array indices.
  2. Try the middle capacity, 8. If it works, every bigger capacity works too; if not, every smaller one fails too. Either way one check rules out half the range.
  3. Pack greedily: fill each day until the next package would go over 8. That takes 2 days, within the limit of 3.
  4. 8 works, but a smaller capacity might too. Keep 8 as a candidate: hi = 8.
  5. The answer is in [4, 8]. Try the middle capacity, 6.
  6. Pack greedily: fill each day until the next package would go over 6. That takes 2 days, within the limit of 3.
  7. 6 works, but a smaller capacity might too. Keep 6 as a candidate: hi = 6.
  8. The answer is in [4, 6]. Try the middle capacity, 5.
  9. Pack greedily: fill each day until the next package would go over 5. That takes 3 days, within the limit of 3.
  10. 5 works, but a smaller capacity might too. Keep 5 as a candidate: hi = 5.
  11. The answer is in [4, 5]. Try the middle capacity, 4.
  12. Pack greedily: fill each day until the next package would go over 4. That takes 4 days, more than the 3 allowed.
  13. 4 is too small, and so is every capacity below it. lo = 5.
  14. lo === hi === 5: the smallest capacity that ships everything within 3 days, found with 4 checks out of 9 candidates.
// Template 4: binary search on the answer. The answer is a number in [lo, hi], and "does x work?"
// flips from false to true exactly once as x grows. Here: the smallest ship capacity that delivers
// every package, in order, within `days` days.
export function shipWithinDays(weights, days) {
  const daysNeeded = (cap) => {
    let used = 1;
    let load = 0;
    for (const w of weights) {
      if (load + w > cap) {   // this package doesn't fit today: start a new day
        used++;
        load = 0;
      }
      load += w;
    }
    return used;
  };
  let lo = Math.max(...weights);                // anything smaller can't carry the heaviest package
  let hi = weights.reduce((a, b) => a + b, 0);  // everything in one day always works
  while (lo < hi) {
    const mid = Math.floor((lo + hi) / 2);
    if (daysNeeded(mid) <= days) hi = mid;      // works: the answer is mid or smaller
    else lo = mid + 1;                          // too small: the answer is bigger
  }
  return lo;
}

// "Maximize the minimum": the largest gap such that k items (2 <= k <= positions.length) can sit at
// distinct positions with every two of them at least `gap` apart. Here "works" goes true...true, false...false,
// so search for the first gap that fails and step back by one.
export function maxMinDistance(positions, k) {
  const sorted = [...positions].sort((a, b) => a - b);
  const canPlace = (gap) => {
    let placed = 1;
    let last = sorted[0];
    for (const p of sorted) {
      if (p - last >= gap) {  // greedy: take the earliest position that is far enough
        placed++;
        last = p;
      }
    }
    return placed >= k;
  };
  let lo = 1;                                   // a gap of 1 always works
  let hi = sorted.at(-1) - sorted[0] + 1;       // wider than the whole span: always fails
  while (lo < hi) {
    const mid = Math.floor((lo + hi) / 2);
    if (!canPlace(mid)) hi = mid;               // first failing gap is mid or smaller
    else lo = mid + 1;
  }
  return lo - 1;                                // the last gap that works
}

Try it

try-it.js
// Edit the inputs and press Run. The functions are the templates above.
const nums = [1, 3, 3, 3, 5, 8, 9];
console.log(binarySearch(nums, 8));                     // 5
console.log(binarySearch(nums, 4));                     // -1
console.log(lowerBound(nums, 3), upperBound(nums, 3));  // 1 4  (the 3s fill indices 1 to 3)
console.log(upperBound(nums, 3) - lowerBound(nums, 3)); // 3    (how many 3s)
console.log(lowerBound(nums, 4));                       // 4    (where 4 would be inserted)

console.log(shipWithinDays([4, 2, 3, 1, 2], 3));        // 5

Complexity

Read more · 37 words
VariantTimeExtra space
Exact match, bounds, rotatedO(log n)O(1)
On the answer: range R, each check O(n)O(n log R)O(1)
Recursive versionsame timeO(log n) call stack

JavaScript you'll use

JavaScript refresher: Numbers, Math and integer tricks Arrays: create, read, update
Read more · 47 words

Math.max(...arr) throws a RangeError above about 100,000 elements: use reduce for huge inputs.

const mid = Math.floor((lo + hi) / 2);     // exact for any integers up to 2^53
const mid2 = lo + ((hi - lo) >> 1);        // also fine while hi - lo < 2^31
(1500000000 + 1600000000) >> 1;           // -597483648: >> works on 32-bit integers
Math.ceil(pile / speed);                   // hours for a pile: a partial hour counts as one
Math.max(...weights);                      // largest value (spreads every element as an argument)
nums.reduce((a, b) => a + b, 0);           // total, a common upper bound for answer searches

Interview questions

Do you need lo + (hi - lo) / 2 in JavaScript?

No: Math.floor((lo + hi) / 2) is exact. The real bug is (lo + hi) >> 1, negative past 2.1 billion.

What changes in a rotated array with repeated values?

Equal ends hide which half is sorted, so shrink with lo++ and hi--. Worst case O(n).

How do you search for a real-valued answer, like a square root?

Use lo = mid or hi = mid, and stop after a fixed number of rounds.

The sorted array's length is unknown. How do you search it?

Probe 1, 2, 4, 8, … until you pass the target, then binary search the last gap.

How do you search a sorted matrix?

If each row starts after the last ends, treat it as one sorted array. Otherwise walk from the top-right corner.

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. In the first-true template (while (lo < hi)), ok(mid) is true. Why do we set hi = mid rather than hi = mid - 1?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

In real systems

This pattern is not only for coding rounds. Here is where it does the work in a design interview.

Next: Stacks remember what is still waiting for an answer.