GUIDE · Searching

Binary search patterns

halve a MONOTONIC search space each step.

lesson15 sectionsruns in browser37 asserts197 lines

At a glance

WHAT
halve a MONOTONIC search space each step.
TELL
"sorted", "O(log n)", "rotated sorted", "peak", "first/last occurrence", "minimum X such that condition holds", "min capacity / max speed / split into k".
SAY
lower bound (first >= target)upper bound (first > target)invariantpredicate / feasibility function"binary search on the answer"monotone predicate
BIG-O
O(log n) time, O(1) space; on-the-answer = O(n log(range)).

The idea#

Picture a phone book. You want "Nguyen." You open the middle. The name on that page comes before Nguyen, so every page on the left is too early. You set it aside and open the middle of what is left. Each look throws away half the remaining pages. The middle page is useful because every name before it is earlier and every name after it is later. The book is monotonic: as you move right, names only move forward in the alphabet.

In code the phone book is any range with that property. You keep two bounds, lo and hi. The answer, if it exists, sits between them. You test the middle. One comparison moves one bound, and the range shrinks by half. You stop when the bounds meet or the middle is the thing you wanted.

Three templates live on that same picture. Template A looks for an exact match on a closed interval. lo and hi are both legal indexes, and the loop runs while lo <= hi. When the middle name is too early, lo = mid + 1. When it is too late, hi = mid - 1. Template B looks for the first index where a yes-or-no predicate becomes true, on a half-open interval [lo, hi). The loop runs while lo < hi. When the middle might still be the answer, you set hi = mid and keep that page. Lower bound is "first index >= target." Upper bound is "first index > target." Template C searches the answer, the way you would search for the slowest eating speed that still finishes the bananas. lo is the smallest answer you would accept, hi is the largest. You ask whether the middle answer works. The predicate has to be monotone: once a value works, every larger value works, or the other way around if you are maximizing.

Why it matters#

A linear scan of a million sorted pages is a million looks. Halving is about twenty. You reach for this when the problem says the input is sorted, asks for a logarithmic bound, or talks about a rotated sorted array, a peak, or the first and last occurrence. You also reach for it when the question is "minimum X such that a condition holds": minimum capacity, maximum speed, split into k parts. There the array is the cost of checking one candidate. The thing you halve is the answer.

Plain search is O(log n) time and O(1) space. Search on the answer is O(n log(range)). The usual bug is the bounds: closed versus half-open, and whether mid stays inside the range. Compute mid as lo + ((hi - lo) >> 1) so the index stays an integer.

A small example#

JS
function findName(pages, name) {  let lo = 0;  let hi = pages.length - 1;  while (lo <= hi) {    const mid = lo + ((hi - lo) >> 1);    if (pages[mid] === name) return mid;    if (pages[mid] < name) lo = mid + 1;    else hi = mid - 1;  }  return -1;} const page = findName(['ann', 'bea', 'cy', 'dee', 'ed'], 'cy');

That is Template A. page is 2. A missing name returns -1 once the two bounds cross.

What the code below is doing#

The sheet writes Template A and Template B before any problem, so you can see the loop you are copying. Classic binary search is Template A. Lower bound and upper bound are Template B. Search-insert position is the lower bound under another name. First bad version is Template B over versions 1..n, with the predicate "is this version bad?"

Then the book has been rotated: a sorted run, cut, and the right piece moved to the front. One half is always still sorted. You test whether the target sits in that sorted half and throw the other half away. Finding the minimum compares mid to hi and walks into the unsorted half, which is where the drop lives. A peak climbs toward the larger neighbor. If a[mid] < a[mid + 1], the peak is on the right.

Template C is binary search on the answer. Koko eating bananas finds the minimum speed whose hour count fits the limit. Shipping within D days and splitting an array into k parts share that shape: lo is the heaviest single piece, hi is the total, and "feasible" means the number of days or pieces you would need is small enough. A fully sorted 2D matrix is one flat phone book. You binary-search the index 0 .. mn - 1 and turn it back into a row and a column with division and remainder, O(log(mn)). Integer square root asks for the largest m with m * m <= x, upper-bound style.

When you talk about it, say this: halve a monotonic range, name the template, and keep the bounds honest.

Everything below is the demonstration. Same idea, written as code you can run.

JSalgorithms/binary-search.js · setup
'use strict';const assert = require('node:assert/strict');
01

TEMPLATE A: exact match (closed interval, lo <= hi)#

JS
//   lo = 0, hi = n-1//   while lo <= hi://     mid = lo + ((hi - lo) >> 1)        // avoids overflow; >> 1 is integer halving//     if a[mid] === target: return mid//     if a[mid] < target: lo = mid + 1 else hi = mid - 1//   return -1//
02

TEMPLATE B: first index where predicate(i) is true (half-open [lo, hi), lo < hi)#

JS
//   lo = 0, hi = n                      // hi = n means "none"//   while lo < hi://     mid = lo + ((hi - lo) >> 1)//     if pred(mid): hi = mid             // mid could be the answer; keep it//     else: lo = mid + 1//   return lo//   -> lower bound = pred(i): a[i] >= t ; upper bound = pred(i): a[i] > t
01

Classic binary search#

template A

O(log n) / O(1)
JS
function binarySearch(a, target) {  let lo = 0, hi = a.length - 1;  while (lo <= hi) {    const mid = lo + ((hi - lo) >> 1);    if (a[mid] === target) return mid;    if (a[mid] < target) lo = mid + 1; else hi = mid - 1;  }  return -1;}
Verified by 3 asserts
JS
assert.equal(binarySearch([-1, 0, 3, 5, 9, 12], 9), 4);assert.equal(binarySearch([-1, 0, 3, 5, 9, 12], 2), -1);assert.equal(binarySearch([], 1), -1);
02

Lower bound / upper bound#

template B

O(log n) / O(1)
JS
function lowerBound(a, t) { // first i with a[i] >= t  let lo = 0, hi = a.length;  while (lo < hi) { const m = lo + ((hi - lo) >> 1); if (a[m] >= t) hi = m; else lo = m + 1; }  return lo;}function upperBound(a, t) { // first i with a[i] > t  let lo = 0, hi = a.length;  while (lo < hi) { const m = lo + ((hi - lo) >> 1); if (a[m] > t) hi = m; else lo = m + 1; }  return lo;}
Verified by 4 asserts
JS
assert.equal(lowerBound([1, 2, 2, 2, 5], 2), 1);assert.equal(upperBound([1, 2, 2, 2, 5], 2), 4); // count of 2s = upper - lower = 3assert.equal(lowerBound([1, 3], 4), 2); // not found -> nassert.equal(lowerBound([], 1), 0);
03

Search insert position#

= lower bound

O(log n)
JS
const searchInsert = lowerBound;
Verified by 3 asserts
JS
assert.equal(searchInsert([1, 3, 5, 6], 5), 2);assert.equal(searchInsert([1, 3, 5, 6], 2), 1);assert.equal(searchInsert([1, 3, 5, 6], 7), 4);
04

First bad version#

template B over versions 1..n with pred = isBad

O(log n)
JS
function firstBadVersion(n, isBad) {  let lo = 1, hi = n;  while (lo < hi) { const m = lo + ((hi - lo) >> 1); if (isBad(m)) hi = m; else lo = m + 1; }  return lo;}
Verified by 3 asserts
JS
assert.equal(firstBadVersion(5, (v) => v >= 4), 4);assert.equal(firstBadVersion(1, (v) => v >= 1), 1);assert.equal(firstBadVersion(10, (v) => v >= 10), 10);
05

Search in rotated sorted array#

one half is always sorted; check if target is inside it

O(log n)
JS
function searchRotated(a, t) {  let lo = 0, hi = a.length - 1;  while (lo <= hi) {    const m = lo + ((hi - lo) >> 1);    if (a[m] === t) return m;    if (a[lo] <= a[m]) { // left half sorted      if (a[lo] <= t && t < a[m]) hi = m - 1; else lo = m + 1;    } else { // right half sorted      if (a[m] < t && t <= a[hi]) lo = m + 1; else hi = m - 1;    }  }  return -1;}
Verified by 3 asserts
JS
assert.equal(searchRotated([4, 5, 6, 7, 0, 1, 2], 0), 4);assert.equal(searchRotated([4, 5, 6, 7, 0, 1, 2], 3), -1);assert.equal(searchRotated([1], 1), 0);
06

Find minimum in rotated sorted array#

compare mid to hi; min is in the unsorted half

O(log n)
JS
function findMin(a) {  let lo = 0, hi = a.length - 1;  while (lo < hi) {    const m = lo + ((hi - lo) >> 1);    if (a[m] > a[hi]) lo = m + 1; else hi = m;  }  return a[lo];}
Verified by 3 asserts
JS
assert.equal(findMin([3, 4, 5, 1, 2]), 1);assert.equal(findMin([1, 2, 3]), 1); // not rotatedassert.equal(findMin([2, 1]), 1);
07

Peak element#

climb toward the larger neighbor; a[m] < a[m+1] -> peak on the right

O(log n)
JS
function findPeak(a) {  let lo = 0, hi = a.length - 1;  while (lo < hi) {    const m = lo + ((hi - lo) >> 1);    if (a[m] < a[m + 1]) lo = m + 1; else hi = m;  }  return lo;}
Verified by 3 asserts
JS
assert.equal(findPeak([1, 2, 3, 1]), 2);assert.ok([1, 5].includes(findPeak([1, 2, 1, 3, 5, 6, 4])));assert.equal(findPeak([7]), 0);
10

TEMPLATE C: binary search on the answer#

JS
//   lo = smallest possible answer, hi = largest possible answer//   while lo < hi: mid = ...; if feasible(mid): hi = mid else lo = mid + 1   // minimize//   feasible() is a MONOTONE predicate: if x works, every x' > x works (or vice-versa for maximize).
08

Koko eating bananas#

min speed k such that hours(k) <= h

O(n log max)
JS
function minEatingSpeed(piles, h) {  const feasible = (k) => piles.reduce((t, p) => t + Math.ceil(p / k), 0) <= h; // predicate  let lo = 1, hi = Math.max(...piles);  while (lo < hi) { const m = lo + ((hi - lo) >> 1); if (feasible(m)) hi = m; else lo = m + 1; }  return lo;}
Verified by 3 asserts
JS
assert.equal(minEatingSpeed([3, 6, 7, 11], 8), 4);assert.equal(minEatingSpeed([30, 11, 23, 4, 20], 5), 30);assert.equal(minEatingSpeed([1], 1), 1);
09

Min capacity to ship within D days#

lo = max(w), hi = sum(w); feasible = days needed <= D

O(n log sum)
JS
function shipWithinDays(weights, D) {  const feasible = (cap) => { // greedy count of days at this capacity    let days = 1, load = 0;    for (const w of weights) { if (load + w > cap) { days++; load = 0; } load += w; }    return days <= D;  };  let lo = Math.max(...weights), hi = weights.reduce((a, b) => a + b, 0);  while (lo < hi) { const m = lo + ((hi - lo) >> 1); if (feasible(m)) hi = m; else lo = m + 1; }  return lo;}
Verified by 3 asserts
JS
assert.equal(shipWithinDays([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 5), 15);assert.equal(shipWithinDays([3, 2, 2, 4, 1, 4], 3), 6);assert.equal(shipWithinDays([5], 1), 5);
10

Split array largest sum (into k parts)#

same shape as shipping; feasible = pieces needed <= k

O(n log sum)
JS
function splitArray(nums, k) {  const feasible = (cap) => {    let parts = 1, sum = 0;    for (const x of nums) { if (sum + x > cap) { parts++; sum = 0; } sum += x; }    return parts <= k;  };  let lo = Math.max(...nums), hi = nums.reduce((a, b) => a + b, 0);  while (lo < hi) { const m = lo + ((hi - lo) >> 1); if (feasible(m)) hi = m; else lo = m + 1; }  return lo;}
Verified by 3 asserts
JS
assert.equal(splitArray([7, 2, 5, 10, 8], 2), 18);assert.equal(splitArray([1, 2, 3, 4, 5], 2), 9);assert.equal(splitArray([1], 1), 1);
11

Search a 2D matrix (rows sorted, row[0] > prev row last)#

treat as flat array; r = i / cols, c = i % cols

O(log(mn))
JS
function searchMatrix(mat, t) {  if (mat.length === 0) return false;  const rows = mat.length, cols = mat[0].length;  let lo = 0, hi = rows * cols - 1;  while (lo <= hi) {    const m = lo + ((hi - lo) >> 1), v = mat[(m / cols) | 0][m % cols];    if (v === t) return true;    if (v < t) lo = m + 1; else hi = m - 1;  }  return false;}
Verified by 3 asserts
JS
assert.equal(searchMatrix([[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], 3), true);assert.equal(searchMatrix([[1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60]], 13), false);assert.equal(searchMatrix([], 1), false);
12

sqrt(x) (integer floor)#

largest m with m*m <= x; upper-bound style

O(log x)
JS
function mySqrt(x) {  let lo = 0, hi = x;  while (lo < hi) {    const m = lo + ((hi - lo + 1) >> 1); // bias up when searching for the LAST true    if (m * m <= x) lo = m; else hi = m - 1;  }  return lo;}
Verified by 3 asserts
JS
assert.equal(mySqrt(8), 2);assert.equal(mySqrt(16), 4);assert.equal(mySqrt(0), 0);

See also