Binary Search
One-liner: whenever a yes/no question flips from "no" to "yes" exactly once along an ordered range, you can find the flip point by repeatedly throwing away half the range.
The analogy
You are guessing a number from 1 to 100 and the friend only says "higher" or "lower". You never guess 1, 2, 3 — you guess 50, and each answer deletes half of the possibilities. After 7 guesses you are done. The key is not that the data is "a sorted array"; it is that each answer lets you discard a whole side.
Recognition signals
Reach for binary search when you see any of these:
- The input is sorted (or sorted-then-rotated) and you need to find a position, a boundary, or a minimum.
- The required complexity is O(log n), or n is so large (10^9 and up) that scanning is impossible.
- The answer is a number you can test: "what is the smallest X such that this works?" and checking one X is easy. Bigger X never makes a working X stop working.
Step-by-step walkthrough
Take the sorted list 1, 3, 5, 7, 9, 11 and target 7. We look for the first index whose value is at least 7.
- The live range is every index,
lo = 0up to (but not including)hi = 6. - Probe the middle,
mid = 3, value7. Is7 >= 7? Yes. Index 3 might be the answer, so keep it and cut everything right of it:hi = 3. - Probe
mid = 1, value3. Is3 >= 7? No. Index 1 and everything left of it is too small:lo = 2. - Probe
mid = 2, value5. No, solo = 3. lo == hi == 3: the range is empty andlois the boundary. Check thatnums[3]really equals 7.
Code template
Most off-by-one bugs come from inventing a new loop for every problem. Use one shape for everything: find the first index where a monotonic condition is true. Searching for a value, a lower bound, an upper bound and "search on answer" are all this same function with a different condition.
// firstTrue returns the smallest i in [0, n) with pred(i) == true, or n if none.
// pred must be monotonic: false, false, ..., true, true.
//
// Invariant: everything left of lo is false, everything at or right of hi is true.
// The answer is always in [lo, hi]. The range is half-open: hi itself is never probed.
func firstTrue(n int, pred func(i int) bool) int {
lo, hi := 0, n
for lo < hi {
mid := lo + (hi-lo)/2 // lower middle, no overflow; mid < hi always
if pred(mid) {
hi = mid // mid might be the answer: keep it
} else {
lo = mid + 1 // mid is false: discard it
}
}
return lo // lo == hi: the boundary
}Why each part exists:
lo, hi := 0, nThe range is half-open: lo is a candidate, hi is one past the last candidate. Starting hi at n (not n-1) allows the answer "none of them", which is what makes insert positions and "not found" fall out for free.
for lo < hiLoop while the range is non-empty. It ends when lo == hi, at which point there is exactly one place the boundary can be. No <=, no special cases.
mid := lo + (hi-lo)/2This picks the lower middle, so mid < hi always: you never probe the excluded end. It also cannot overflow, unlike (lo+hi)/2 in languages with fixed-width ints.
if pred(mid) { hi = mid }If the condition is true at mid, then mid could itself be the first true, so it must stay in the range. This is why it is hi = mid and not mid - 1.
else { lo = mid + 1 }If it is false at mid, then mid and everything left of it are false. Discard mid itself with + 1. That + 1 guarantees progress, so the loop can never hang.
The invariant that holds before and after every iteration: everything left of lo is false, everything from hi on is true. If you can state that sentence about your loop, it is correct.
See it run
Watch the live range shrink. Cells outside [lo, hi) are dimmed — they have been proven irrelevant. Try the duplicates preset: the search lands on the first copy.
- target
- 7
- lo
- 0
- hi
- 6
- mid
- —
The live range is [lo, hi) = [0, 6). Invariant: everything left of lo is below 7; everything from hi on is at least 7. hi = 6 is just past the end (no cell).
// Search: index of target in a sorted slice, or -1.
// Find the first index whose value is >= target, then check it really is target.
func Search(nums []int, target int) int {
lo, hi := 0, len(nums)
for lo < hi {
mid := lo + (hi-lo)/2
if nums[mid] >= target {
hi = mid
} else {
lo = mid + 1
}
}
if lo < len(nums) && nums[lo] == target {
return lo
}
return -1
}The classic search
// Search: index of target in a sorted slice, or -1.
// Find the first index whose value is >= target, then check it really is target.
func Search(nums []int, target int) int {
lo, hi := 0, len(nums)
for lo < hi {
mid := lo + (hi-lo)/2
if nums[mid] >= target {
hi = mid
} else {
lo = mid + 1
}
}
if lo < len(nums) && nums[lo] == target {
return lo
}
return -1
}It finds the first index with a value at least target, then makes one final check that the value is really there. Searching for an exact match inside the loop and returning early is the version that tends to break; this one cannot.
Variations
Lower and upper bounds — the first index with >= target and the first index with > target. Their difference is how many copies exist, and the second one minus one is the last occurrence:
// SearchRange: first and last index of target, or [-1, -1].
// Last occurrence = (first index with value > target) - 1.
func SearchRange(nums []int, target int) []int {
first := firstTrue(len(nums), func(i int) bool { return nums[i] >= target })
if first == len(nums) || nums[first] != target {
return []int{-1, -1}
}
after := firstTrue(len(nums), func(i int) bool { return nums[i] > target })
return []int{first, after - 1}
}
// SearchInsert: index where target is, or would be inserted.
func SearchInsert(nums []int, target int) int {
return firstTrue(len(nums), func(i int) bool { return nums[i] >= target })
}Search on the answer space — there is no array. Instead the candidates are the numbers 1 to the largest pile, and the condition is "can she finish in time at this speed?". Faster never hurts, so it is monotonic:
// MinEatingSpeed: smallest speed k so every pile is eaten within h hours.
// Search the ANSWER space [1, max(piles)]: "can finish at speed k" is false..false,true..true.
func MinEatingSpeed(piles []int, h int) int {
maxPile := 0
for _, p := range piles {
maxPile = max(maxPile, p)
}
// candidate speed for index i is i+1
i := firstTrue(maxPile, func(i int) bool {
k, hours := i+1, 0
for _, p := range piles {
hours += (p + k - 1) / k // ceil(p / k)
}
return hours <= h
})
return i + 1
}Same idea, different bounds. The smallest possible capacity is the heaviest package (anything lower can never ship it), and the largest useful one is the total weight:
// ShipWithinDays: least ship capacity that ships all packages in order within days.
// Capacity lies in [max(weights), sum(weights)]; "fits in days" is monotonic in capacity.
func ShipWithinDays(weights []int, days int) int {
lo, total := 0, 0
for _, w := range weights {
lo = max(lo, w)
total += w
}
i := firstTrue(total-lo+1, func(i int) bool {
capacity := lo + i
used, load := 1, 0
for _, w := range weights {
if load+w > capacity {
used++
load = 0
}
load += w
}
return used <= days
})
return lo + i
}Rotated sorted array — the array is not sorted, but the condition nums[i] <= last element is false, false, true, true along it. That finds the minimum, i.e. the rotation point. Then search the array as if it were sorted using a shifted index:
// SearchRotated: target in a rotated sorted slice of distinct values, or -1.
// Step 1: find the rotation point (index of the minimum). Step 2: binary search the right half.
func SearchRotated(nums []int, target int) int {
n := len(nums)
if n == 0 {
return -1
}
// the minimum is the first element that is <= the last element
pivot := firstTrue(n, func(i int) bool { return nums[i] <= nums[n-1] })
// treat the slice as sorted with a shifted index: real = (i + pivot) % n
i := firstTrue(n, func(i int) bool { return nums[(i+pivot)%n] >= target })
if i < n && nums[(i+pivot)%n] == target {
return (i + pivot) % n
}
return -1
}
// FindMin: minimum of a rotated sorted slice of distinct values.
func FindMin(nums []int) int {
n := len(nums)
return nums[firstTrue(n, func(i int) bool { return nums[i] <= nums[n-1] })]
}Complexity
| Approach | Time | Space |
|---|---|---|
| Linear scan | O(n) | O(1) |
| Binary search on an array | O(log n) | O(1) |
| Search on answer space | O(n log R) | O(1) — n = cost of one check, R = size of the answer range |
Each iteration halves the range, so 10^9 candidates need only about 30 probes. For search-on-answer, the total cost is probes times the cost of one check.
Common mistakes
Practice ladder
Ordered Easy → Hard. Before coding, write the yes/no function and say out loud where it flips.
- 1.Binary SearchEasySorted, and you want a position. What is the first index at least the target?
- 2.Search Insert PositionEasyThe answer can be one past the end.
- 3.Find First and Last Position of Element in Sorted ArrayMediumTwo boundaries, two conditions.
- 4.Find Minimum in Rotated Sorted ArrayMediumCompare each element with the last one.
- 5.Search in Rotated Sorted ArrayMedium
- 6.Koko Eating BananasMediumThere is no sorted input. What number is being guessed?
- 7.Capacity To Ship Packages Within D DaysMediumBound the guess between the heaviest item and the total.
- 8.Median of Two Sorted ArraysHardGuess how many elements come from the first array.
Which pattern? Drills
Unlabeled problems — pick the pattern, then read why.
You are given a rotated version of a sorted array of distinct numbers (for example 4,5,6,7,0,1,2). In O(log n) time, find the smallest element.
Which pattern?