Binary search+
Binary search is usually introduced as "find a value in a sorted array". That is only one case. What the method really needs is a yes/no test that flips from false to true exactly once and never flips back. This chapter starts from one template and one interval convention, then works through boundaries, rotated arrays, and peaks, and ends at binary search on the answer: turning a hard optimization problem into a short series of yes/no questions.
One template: name the interval, then derive everything else
The idea is easy. The boundaries are not. Fix a convention that cannot loop forever.
Binary search works on a sorted sequence. Compare the element in the middle with the target. That single comparison tells you whether the answer is in the left half or the right half, so half of the candidates disappear at once. 100 numbers need at most 7 questions (⌈log₂100⌉), and a billion numbers need 30. Try it yourself first:
The idea is easy to state and hard to write. Most people produce an infinite loop or an off-by-one error on the first attempt. This is well documented: Jon Bentley reported in Programming Pearls that when he gave professional programmers a couple of hours to write a binary search, fewer than 10 percent produced a correct one. So do not write it from intuition. Write it from a convention.
Start by naming the interval. This chapter uses the closed interval [lo, hi]: both ends are still candidates. The invariant is that if target is in the array, its index is inside [lo, hi]. Everything already discarded has been proved not to be the answer. The loop condition and both updates follow from that one sentence:
- [lo, hi] is non-empty exactly when
lo <= hi, so that is the loop condition. When lo == hi there is still one element left to check. - If
nums[mid] < target, then mid and every index to its left hold values smaller than target. The answer can only be in [mid+1, hi], solo = mid + 1. - If
nums[mid] > target, the answer can only be in [lo, mid−1], sohi = mid - 1.
Why it terminates: mid always lies inside [lo, hi], and both updates step past mid, so each iteration removes at least the element at mid. hi − lo therefore strictly decreases and the interval reaches empty in a finite number of steps. Each iteration also removes about half of what is left, so the loop runs about log₂n times. Time O(log n). The iterative version keeps only two numbers, so extra space is O(1); written recursively, the call stack is O(log n) deep.
There is a second convention: the half-open interval [lo, hi), where hi is one position past the last candidate. Derive it the same way. [lo, hi) is non-empty when lo < hi, so the loop is while (lo < hi). Discarding the right part is hi = mid, not mid - 1, because hi is already outside the interval. Discarding the left part is still lo = mid + 1. Both conventions are correct. Mixing them is the classic bug — while (lo <= hi) together with hi = mid never ends. Pick one convention and derive the rest from it. Everything below uses the closed interval.
Here is the plain exact search (LC 704) written that way:
lo + hi cannot overflow and (lo + hi) // 2 is safe here. Keeping the lo + (hi - lo) // 2 habit still pays off, because the same code translated to Java or C++ stays correct.With the closed interval [lo, hi] the loop must be lo <= hi, because lo == hi still leaves one element to check. Using lo < hi skips it. The half-open form has its own matching set. Pick one and stay with it.
In a language with fixed-width integers, such as Java or C++, (lo + hi) / 2 can overflow. Write lo + (hi - lo) / 2: same value, no overflow. Python integers cannot overflow, and JavaScript numbers are exact up to 2⁵³ — but >> truncates to 32 bits there, so do not use it.
mid rounds down, so mid == lo whenever hi == lo or hi == lo+1. With while (lo <= hi), hi = mid rewrites hi with the value it already had once lo == hi: infinite loop. With while (lo < hi), lo = mid does the same once hi == lo+1. Every branch must move past mid.
The bug that hid for nine years
In 2006 Joshua Bloch published Extra, Extra — Read All About It: Nearly All Binary Searches and Mergesorts Are Broken. He pointed out that java.util.Arrays.binarySearch computed (low + high) / 2, which overflows into a negative number once the array is large enough. The code had been shipping for about nine years, and the same line had been copied into many textbooks. The fix was one line: low + (high - low) / 2. In binary search, the hard part is always the boundaries.
Boundaries: lower_bound and upper_bound
MEDIUMWorked example A · LC 34 — with duplicates, where do the first and last copies sit?
The exact search has a weakness. When the array contains duplicates, it returns whichever matching index it happened to land on. Many problems need the first or the last one instead. Two precise tools cover almost every such question:
- lower_bound(t) is the index of the first element ≥ t. It equals the number of elements smaller than t. If every element is smaller than t, it returns
n, the array length — one position past the end. - upper_bound(t) is the index of the first element > t. It equals the number of elements ≤ t. If every element is ≤ t, it also returns
n.
Everything else follows. The number of copies of t is upper_bound(t) − lower_bound(t); if that is 0, t is absent. The first occurrence is lower_bound(t), but only after checking that this index is below n and that the value there really is t. If it is, the last occurrence is upper_bound(t) − 1.
Both are written with one template: closed interval plus a candidate variable. When the test passes, record the current mid in ans, then keep shrinking toward the side you want. The invariant is: ans is the best index found so far, and any better one is still inside [lo, hi]. Initialize ans to n so that "nothing matched" comes out correctly.
LC 34: in a sorted array, return the first and last index of target, or [−1, −1] if it is absent. Brute force: find any match, then walk left and right — that is O(n) when the array is all target, and it wastes the sorted order. Solution: the left end is lower_bound(target); the right end is the first index greater than target, minus one. Step through it:
Note the shortcut in the animation. "First index > 8" is the same position as "first index ≥ 9", so upper_bound(t) = lower_bound(t + 1). That rewrite is only valid because the values are integers, where t + 1 is the next possible value; on floating-point data you would need a real upper_bound. With integers, one lower_bound function produces both ends:
bisect.bisect_left is lower_bound and bisect.bisect_right is upper_bound. You may call them in an interview, but write the loop by hand at least once — that is where the boundaries become clear.Complexity and the usual follow-up questions
Two binary searches: O(log n) time, O(1) space. Common follow-ups: (1) "Can one function give both ends?" — yes, lower_bound(t) and lower_bound(t+1)-1. (2) "How many times does target occur?" — right − left + 1, or equivalently upper_bound − lower_bound. (3) "What about LC 35, search insert position?" — the answer is lower_bound(target) directly, with no existence check at all. Same template, three uses.
What binary search actually requires
MEDIUMWorked example B · LC 33 — the array is scrambled, so why can you still halve it?
Time to correct a common belief. Binary search does not require the array to be sorted. What it requires is that one O(1) test tells you which half can be discarded. Put differently: the range must split into a part where the test is false and a part where it is true, with a single flip between them. Sorted order is the most common way to get such a split, but it is not the only way.
LC 33: a sorted array was rotated — for example [0,1,2,4,5,6,7] became [4,5,6,7,0,1,2] — and you must find target and return its index. The array is no longer sorted as a whole, so the plain search fails. The key observation: a rotation creates exactly one drop, so wherever you cut, at least one of the two halves is fully sorted. Decide which half that is, then decide whether target lies inside it, and one half can be discarded safely. LC 33 guarantees that all values are distinct, which keeps the first decision unambiguous:
The test is short: nums[lo] <= nums[mid] means [lo, mid] is sorted; otherwise [mid, hi] is. Then compare target against the two ends of that sorted half. Inside a sorted half a range comparison is reliable; the other half may contain the drop, so it is left for the next iteration.
nums[lo] <= target < nums[mid] reads exactly like the mathematical interval. It is easier to read and harder to get backwards.Compare nums[mid] with the right end nums[hi]: greater means the minimum is strictly to the right (lo = mid+1), otherwise hi = mid. Converge to a single index. Comparing with nums[lo] misjudges an array that was not actually rotated.
When a[lo] == a[mid] == a[hi], as in [1,1,1,0,1], neither half can be shown to be sorted. The split is gone, so the only safe move is lo++, hi--, and the worst case becomes O(n).
The duplicate version of LC 153. When a[mid] == a[hi] there is nothing to decide on, so shrink conservatively with hi--; the minimum stays in range because a copy of a[hi] sits at mid. Worst case O(n).
Why 81 and 154 degrade while 153 does not
Without duplicates, a[lo] and a[mid] always compare to a definite answer, so the split holds on every step and the time stays O(log n). Once duplicates are allowed and a[lo] == a[mid] == a[hi], the same three values are consistent with "the left half is flat and sorted" and with "the drop is hidden among equal values". Nothing distinguishes them, so that step gives up halving and removes one element from each end instead. Duplicates are what break binary search, and saying that clearly is what an interviewer is listening for when they ask "what if there are duplicates?"
Peaks and matrices: two more sources of the split
MEDIUMHalving without sorted order: from the direction of the slope, and from row and column order.
LC 162, find a peak: return the index of any element larger than both of its neighbors. Neighboring values are never equal, and positions outside the array count as −∞. The array has no order at all, yet the direction of the slope gives the split. If nums[mid] < nums[mid+1], the values are rising at mid, and a peak must exist in [mid+1, hi]: either they keep rising all the way to hi, which is then a peak because its right neighbor is −∞, or they stop rising at some index, which is then a peak. Otherwise a peak exists in [lo, mid]. So you can halve the range by always walking uphill:
while lo < hi guarantees mid < hi, and hi is the last index, so mid + 1 is at most hi. The index never goes past the end of the array.Now two dimensions. Matrix search comes in two genuinely different forms, and the difference is how far the ordering reaches:
Each row increases, and the first value of a row is greater than the last value of the row above. Reading m×n row by row gives one strictly increasing array. Search [0, mn−1] and map back with mid/n and mid%n. O(log mn).
Only each row and each column is sorted; rows do not connect. Flattening gives no order, so a single search would miss values. Stand at the top-right corner: too large means move left (drop a column), too small means move down (drop a row). O(m + n).
Binary search on the answer
MEDIUMWorked example C · LC 875 Koko — when computing the answer is hard, ask a series of yes/no questions instead.
So far the search ran over array indices. Now it runs over the answer itself. The rule is one sentence: when computing the best answer is hard but checking a candidate answer is easy, search the answer. The condition is that the check must be monotonic: there is a point where the yes/no test flips from false to true and it never flips back. Drawn out, it is a line of F…F followed by T…T. Note what is not required: the input array does not have to be sorted. Only the predicate has to be monotonic.
What you are looking for is the highlighted flip point, the first candidate that passes. That is exactly the lower_bound from §02, with one substitution: instead of comparing with target, you call a predicate you wrote yourself, usually named judge or check.
LC 875: Koko has several piles of bananas, piles, and the guards come back in h hours. Each hour she picks one pile and eats k bananas from it; if the pile runs out she waits for the next hour rather than starting another pile. Find the smallest speed k that finishes within h hours. Brute force: try k = 1, 2, 3, … and stop at the first one that works — O(max × n), and max can be 10⁹, so it is too slow. Why binary search applies: a higher speed never needs more hours, so judge(k) = Σ⌈pile/k⌉ ≤ h is monotonic in k. Search the value range [1, max(piles)] for the first k that passes. The F/T line drawn above is this exact problem, with piles = [3,6,7,11] and h = 8:
-(-p // k) and math.ceil(p / k) also round up, but (p + k - 1) // k stays in integers and is the safest of the three. Python integers are arbitrary precision, so the sum cannot overflow.In production: answer search shows up in capacity planning
The pattern of "guess a value, then verify it" is common in real systems. Load testing uses it to find the highest request rate a service still survives. Adaptive video players use it to pick the highest bitrate that does not stall. Query planners and compilers use it to find the smallest degree of parallelism that meets a latency budget. In every case computing the optimum directly is hard, while checking one configuration is easy — so the search runs over the space of answers.
Three questions to ask out loud in an interview
When a problem says "maximize the minimum", "minimize the maximum", or "find the extreme value that satisfies a condition", say these three things: (1) "What is the range of the answer?" — that fixes lo and hi. (2) "Given a candidate x, can I check it in O(n)?" — that is the judge function. (3) "Is the check monotonic in x?" — that is the F…F T…T line. Three yes answers mean binary search on the answer, at O(n · log(range)).
More answer search: one method, three appearances
Minimize the maximum (LC 1011, LC 410) and find the largest feasible value (LC 69, LC 367).
Once LC 875 is clear, the problems below are the same method in different words. Start with the standard "minimize the maximum" problem, LC 1011: packages on a conveyor belt must be loaded in their given order, and you need the smallest ship capacity that gets them all delivered within D days. judge(cap) loads packages one by one and starts a new day whenever the next one does not fit, then compares the day count with D. The lower end of the range is the heaviest package — below that, that package can never be loaded at all — and the upper end is the sum of all weights, which ships everything in one day.
The last shape is the square root (LC 69): compute ⌊√x⌋, the largest integer k with k×k ≤ x. That predicate is monotonic too, but it runs the other way: it is true for small k and false for large k, so you want the last value that passes. Record the candidate, then push right with lo = mid + 1 — the mirror image of LC 875. Keep the two directions apart:
mid * mid is always exact. LC 367 is the same search with a different exit: return true on mid * mid == num, and false once the interval is empty.| Template | Interval | Loop | When the test passes | Returns | Problems |
|---|---|---|---|---|---|
| Exact search | [lo, hi] | lo <= hi | equal -> return mid | index / −1 | 704 · 74 |
| Boundary (first feasible) | [lo, hi] | lo <= hi | ans=mid; hi=mid−1 | ans | 34 · 35 · 875 · 1011 |
| Boundary (last feasible) | [lo, hi] | lo <= hi | ans=mid; lo=mid+1 | ans | 69 · 367 |
| Converging | [lo, hi] | lo < hi | mid+1 on one side, mid on the other | lo | 153 · 162 · 852 |
Ordinary binary search vs binary search on the answer
Ordinary binary search looks for a position in data that is already sorted; the range it searches is the set of array indices. Binary search on the answer guesses inside the range of possible answers, from the smallest to the largest one that could be correct, and the input array may not be sorted at all. The test also changes: instead of comparing with target, you call a judge function you wrote. That is what takes binary search from "look something up" to "solve an optimization problem".
Problem set: 18 binary search problems
Core setGrouped as template, boundaries, monotonic split, and answer search, from easier to harder. Think for 30 seconds before opening the hint.
Chapter quiz
✎ QuizAnswer all 9 correctly to mark this chapter as complete.
Why is mid = lo + (hi − lo) / 2 preferred over (lo + hi) / 2?
A search uses the closed interval [lo, hi] and mid = lo + (hi − lo) / 2. Which pair of loop condition and update runs forever?
A binary search returns the first index whose value is >= target (lower_bound). What does it return when target is not in the array?
In the sorted array [5,7,7,8,8,8,10], how many times does 8 occur? (Work it out as upper_bound(8) − lower_bound(8).)
A rotated sorted array such as [4,5,6,7,0,1,2] is not sorted as a whole. Why can binary search still work on it?
What is the worst-case time of LC 81 (rotated array search with duplicates), and why?
Which of these are binary search on the answer — guess a candidate answer, then verify it with a monotonic yes/no test? (Select all that apply)
In LC 875 the test is "can Koko finish within h hours at speed k?". Which statement about its monotonicity is correct?
LC 74 (rows increasing, and the first value of each row larger than the last value of the row above) can be solved with one binary search, but LC 240 (rows and columns each increasing, no relation between rows) cannot. Why?
- Binary search does not require a sorted array. It requires one O(1) test that says which half to discard — a range that splits into a false part and a true part with a single flip between them. Sorted order is the most common source of that split, not the only one.
- Name the interval first, then derive the rest. Closed interval [lo, hi] goes with
lo <= hi,hi = mid - 1, andlo = mid + 1. Half-open [lo, hi) goes withlo < hiandhi = mid. Mixing the two is the classic bug. - The invariant is: if the answer exists, its index is inside [lo, hi]. Every claim about correctness traces back to it, and every branch must move past mid so the interval strictly shrinks and the loop ends.
- Overflow is language-specific. In Java or C++,
(lo + hi) / 2can overflow, so writelo + (hi - lo) / 2. Python integers are arbitrary precision. JavaScript numbers are exact to 2⁵³, so plain division is fine, but(lo + hi) >> 1truncates to 32 bits and is wrong for large ranges. - Boundary search = record a candidate, then keep pushing to one side. lower_bound(t) is the first index ≥ t, upper_bound(t) the first index > t; both return n when nothing qualifies, and for integers upper_bound(t) = lower_bound(t+1).
- Rotated arrays: one of the two halves is always sorted, so ask which one and whether target is inside it. Duplicates (LC 81, LC 154) destroy that test and the worst case becomes O(n).
- Binary search on the answer: when solving is hard, check instead. If the yes/no test on a candidate answer is monotonic, search the range of answers and call judge. LC 875, LC 1011, and LC 410 are the same method. Two mirrored directions: first feasible (LC 875, hi = mid−1) and last feasible (LC 69, lo = mid+1).
- Cost: O(log n) time, O(1) extra space when written iteratively, O(log n) stack when written recursively. For an answer search it is O(cost of judge × log(range)).