Binary Search
Halve the search space every step, on a sorted array or on the answer itself, for O(log n) decisions.
- 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.
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
Tap the signals you can see in the problem.
RemembersortedrotatedfirstlastMinimumminimize the maximumO(log n)
- Exact match
Search rangeindices
binary search[0, n - 1] - First true (lower bound)
Search rangeindices
insert position[0, n] - Rotated array
Search rangeindices
search rotated[0, n - 1] - 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
- 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
- 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.
- lo <= hi, so the range is not empty yet: 8 candidates, indices 0 to 7.
- The middle of [0, 7] is index 3, rounding down. Compare nums[3] = 17 with 44.
- 17 < 44. The array is sorted, so index 3 and everything left of it are too small. Drop them: lo = 4.
- lo <= hi, so the range is not empty yet: 4 candidates, indices 4 to 7.
- The middle of [4, 7] is index 5, rounding down. Compare nums[5] = 30 with 44.
- 30 < 44. The array is sorted, so index 5 and everything left of it are too small. Drop them: lo = 6.
- lo <= hi, so the range is not empty yet: 2 candidates, indices 6 to 7.
- The middle of [6, 7] is index 6, rounding down. Compare nums[6] = 44 with 44.
- 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:
Name the search space
Indices (up to
nif "insert at the end" is valid), or answer values.Write the yes/no question and check it is monotonic
ok(x)must be false, then true: likenums[i] >= target. Ifok(x)doesn't implyok(x + 1), it's the wrong tool.Pick the template and state the invariant
First true: "everything before
lois false, everything fromhion is true".Update so the range shrinks
Never
lo = midwith a rounded-downmid: it loops forever.Run the tiny cases by hand
Trace 0, 1 and 2 candidates.
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:
| Template | Loop | Updates | Ends with |
|---|---|---|---|
| Exact match | lo <= hi | lo = mid + 1, hi = mid - 1 | lo === hi + 1 (empty) |
| First true | lo < hi | lo = mid + 1, hi = mid | lo === 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 = 1First true: the first index with nums[i] >= target
- 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
- 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.
- The answer is somewhere in [0, 7]. Check the middle: mid = 3, nums[3] = 3.
- 3 >= 3: T. Index 3 might be the first T, so keep it: hi = mid = 3, not mid - 1.
- The answer is somewhere in [0, 3]. Check the middle: mid = 1, nums[1] = 3.
- 3 >= 3: T. Index 1 might be the first T, so keep it: hi = mid = 1, not mid - 1.
- The answer is somewhere in [0, 1]. Check the middle: mid = 0, nums[0] = 1.
- 1 < 3: F. Every index up to 0 is F too (the array is sorted), so the first T is after mid: lo = 1.
- 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
- 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
- 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.
- 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.
- Pack greedily: fill each day until the next package would go over 8. That takes 2 days, within the limit of 3.
- 8 works, but a smaller capacity might too. Keep 8 as a candidate: hi = 8.
- The answer is in [4, 8]. Try the middle capacity, 6.
- Pack greedily: fill each day until the next package would go over 6. That takes 2 days, within the limit of 3.
- 6 works, but a smaller capacity might too. Keep 6 as a candidate: hi = 6.
- The answer is in [4, 6]. Try the middle capacity, 5.
- Pack greedily: fill each day until the next package would go over 5. That takes 3 days, within the limit of 3.
- 5 works, but a smaller capacity might too. Keep 5 as a candidate: hi = 5.
- The answer is in [4, 5]. Try the middle capacity, 4.
- Pack greedily: fill each day until the next package would go over 4. That takes 4 days, more than the 3 allowed.
- 4 is too small, and so is every capacity below it. lo = 5.
- 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
// 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)); // 5Complexity
Read more · 37 words
| Variant | Time | Extra space |
|---|---|---|
| Exact match, bounds, rotated | O(log n) | O(1) |
| On the answer: range R, each check O(n) | O(n log R) | O(1) |
| Recursive version | same time | O(log n) call stack |
JavaScript you'll use
JavaScript refresher: Numbers, Math and integer tricks Arrays: create, read, updateRead 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 searchesInterview 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 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Binary SearchFind the index of a target in a sorted array of distinct numbers in O(log n) time, or report that it is missing.Easy
- Search Insert PositionReturn where a target is in a sorted array, or the index where it would be inserted to keep the array sorted.Easy
- Find First and Last Position of a ValueIn a sorted array with duplicates, find the first and last index of a target in O(log n) time.Medium
- Find Minimum in Rotated Sorted ArrayFind the smallest value in a sorted array of distinct numbers that has been rotated by an unknown amount.Medium
- Search in a Rotated Sorted ArrayA sorted array of distinct numbers was rotated at an unknown point. Find a target's index in O(log n) time.Medium
- Koko Eating BananasFind the slowest eating speed that still finishes every pile of bananas within h hours.Medium
- Split Array Largest SumCut an array into k contiguous pieces so that the largest piece sum is as small as possible.Hard
- Median of Two Sorted ArraysFind the median of two sorted arrays in logarithmic time without merging them.Hard
- Two Sum on a Sorted ArrayFind the two positions in a sorted array whose values add up to a target, using O(1) extra space.Medium
- Minimum Size Subarray SumFind the length of the shortest run of positive numbers whose sum reaches a target.Medium
- Find the Duplicate NumberAn array of n + 1 numbers from 1 to n has exactly one repeated value. Find it without modifying the array and with O(1) extra space.Medium
- Path With Minimum EffortWalk across a height map from corner to corner so that the steepest single step is as gentle as possible.Medium
- Swim in Rising WaterWater rises one unit per second over a grid of distinct heights. When can you first swim from one corner to the other?Hard
- Longest Increasing SubsequenceFind the length of the longest strictly increasing subsequence of an array.Medium
- Time Based Key-Value StoreStore values with timestamps and answer what a key held at any past moment.Medium
- Search a 2D MatrixFind a value in a grid whose rows are sorted and where each row starts above the end of the previous row.Medium
In real systems
This pattern is not only for coding rounds. Here is where it does the work in a design interview.