Chapter 2: Binary Search
Master binary search from its simplest form to its most creative applications: search sorted arrays in O(log n), generalize the technique to monotonic functions and answer spaces, and handle rotated arrays and unimodal peaks where standard approaches fail.
- Topics
- 8
- Videos
- 8
- Min read
- 31
Chapter Overview
Binary search is one of the most powerful algorithmic techniques you will ever learn. At its core, the idea is simple --- if you can rule out half the remaining candidates with a single comparison, you can find your answer in logarithmic time. On an array of one billion elements, binary search needs at most 30 comparisons.
But binary search is far more than searching a sorted array. Once you understand the underlying principle --- exploiting a monotonic property to halve the search space --- you can apply it to estimation problems, optimization problems, and situations where the "array" is not an array at all but a continuous range of real numbers or an abstract function.
This chapter builds your binary search skill from the ground up. You will start with the classic sorted-array search, then learn to recognize the general pattern: a sequence of values that transitions from one state to another. By the end, you will be able to apply binary search to rotated arrays, mountain arrays, and problems where you are searching for the best answer rather than a specific element.
This chapter covers:
- Vanilla Binary Search: The classic O(log n) search on sorted data
- Sorted Boolean Array: Finding the first True in a monotonic boolean sequence
- Monotonic Function Search: Binary search on answer spaces
- Lower Bound / bisect_left: Finding the first element not smaller than a target
- First Occurrence: Locating the leftmost position of a target value
- Square Root Estimation: Binary search on real numbers with precision control
- Rotated Array Minimum: Searching arrays with a single rotation point
- Peak Finding: Locating the maximum in a unimodal array
Chapter Roadmap
Click any topic to jump in
Vanilla Binary Search
The halving principle: a single invariant — target $\in [l, r]$ — gives $O(\log n)$ search on any sorted array.
The boolean pattern explicitly abstracts the decision step; answer-space search abstracts what is being searched.
Sorted Boolean Pattern
Generalise from equality to any monotonic predicate: find the first index where $P(i)$ becomes true.
Binary Search on Answer
Instead of searching indices, search the answer space — as long as feasibility is monotone in the answer.
Once you can find one match, variants locate the first, last, or a boundary.
Lower / Upper Bound
Lower-bound and upper-bound variants — the two primitives that build every range query on a sorted array.
First / Last Occurrence
Finding the leftmost or rightmost matching index by continuing the search *past* a match.
Binary Search on Reals
Halving on continuous domains — iteration count is fixed by target precision, not by $n$.
When the array is rotated or unimodal, local information still guides halving.
Rotated Arrays
Recover order from a rotated sorted array by identifying which half is still sorted each step.
Peaks and Slopes
Use the local slope as the decision function — binary search works on any unimodal landscape.
Topics
8 topics in this chapter
A phone book holds a million names and you need the number for "Moreno". Reading it front to back could take a million glances, but nobody does that: you open near the middle, see "Lopez", and immediately discard the entire first half. Repeat a few more times and you land on the page after about twenty glances.
The previous chapter ended with sorting, including merge sort, quick sort, and custom comparators, and sorting is exactly what makes this trick possible. Once data is ordered, a single comparison with the middle element tells you which half cannot contain the target. That is binary search: it keeps a shrinking range of candidates and halves it every step, so n elements need only about comparisons, 30 for a billion.
This first topic covers the classic version on a sorted array: the halving loop and its invariant, then the details that make it notoriously easy to get wrong, namely loop conditions, off-by-one updates, midpoint overflow, and what the pointers mean when the target is absent. Every later topic in the chapter is a variation on this loop.
Definition
Binary search finds a target in a sorted array by maintaining a range [lo, hi] that must contain the target if it is present. Each step compares the target with the middle element and discards the half that cannot contain it. It runs in O(log n) time and O(1) extra space, returning the index or a not-found signal.
In this topic
- 1The Halving Principle
- 2Implementation Details and Edge Cases
The Halving Principle
Binary search maintains two pointers, lo and hi, that define the current search range. At each step, it computes mid = (lo + hi) // 2 and compares arr[mid] to the target. If arr[mid] == target, the search is done. If arr[mid] < target, the answer must be in the right half, so lo = mid + 1. If arr[mid] > target, the answer must be in the left half, so hi = mid - 1. Each comparison eliminates roughly half the remaining elements, giving O(log n) total comparisons.
Binary search maintains the invariant if the target exists, it lies in . Each iteration either finds the target or discards half of the range, so the size of the range follows . The number of iterations is the smallest with , i.e. . The recurrence has closed form by the master theorem with .
Search for target = 7 in the sorted array [1, 3, 5, 7, 9, 11, 13]. Trace each step.
Implementation Details and Edge Cases
The loop condition while lo <= hi ensures we check every candidate. When lo > hi, the target is not in the array. A common bug is computing mid = (lo + hi) // 2 with very large integers --- in languages with fixed-size integers this can overflow. The safe formula is mid = lo + (hi - lo) // 2. In Python, integers have arbitrary precision so overflow is not a concern, but using the safe formula is good practice for portability.
The overflow trap: can overflow in languages with fixed-width integers; is safe. The termination invariant depends on whether you use with updates (classic, range shrinks strictly), or with updates (half-open, converges to a single index). Both run in — off-by-one bugs come from mixing them.
What does binary search return when the target is not in the array [2, 4, 6, 8, 10] and target = 5?
Related Problems on PixelBank
A software release is broken, and you know it worked in build 1 and fails in build 1000. Somewhere in between, one commit introduced the bug, and every build after it is also broken. Testing each build in order could take a thousand slow test runs, yet the question you are really asking is simple: where does "good" turn into "bad"?
The previous topic, Vanilla Binary Search, found an exact value in a sorted array of numbers. Here we notice that the numbers were never the point. What binary search actually needs is a monotonic yes/no property: a sequence that reads False, False, ..., False, True, ..., True, with every False before every True. Checking the middle tells you which side of the boundary you are on, so about ten tests settle the thousand-build question.
We first build the "find the first True" template, which uses a half-open range and the loop condition lo < hi, and see why it cannot loop forever. Then we handle the edge cases, all False and all True, with a sentinel index instead of special-case code. This template powers nearly every remaining topic in the chapter.
Definition
A sorted boolean array is a sequence of the form [False, ..., False, True, ..., True], meaning a monotonic predicate evaluated over indices. The task is to return the smallest index holding True, or the length of the array if none does. Binary search solves it in O(log n) predicate evaluations by keeping lo on the False side and hi on the True side.
In this topic
- 1The Monotonic Boolean Pattern
- 2Handling Edge Cases
The Monotonic Boolean Pattern
A sorted boolean array has the form [F, F, ..., F, T, T, ..., T]. We want the index of the first True. Binary search works because the array is monotonic: once a True appears, everything to its right is also True. If arr[mid] is True, the first True is at mid or to its left, so hi = mid. If arr[mid] is False, the first True must be to the right, so lo = mid + 1. The loop runs with while lo < hi and returns lo (which equals hi) at the end.
If a predicate is monotone — once it becomes true it stays true — then the set is a suffix . Binary search finds in calls to . The invariant: and throughout, so when the index is exactly the transition point.
Find the index of the first True in [False, False, False, True, True, True].
Handling Edge Cases
Two edge cases arise: (1) the array is entirely False --- there is no True element, and (2) the array is entirely True --- the first True is at index 0. A clean way to handle both is to initialize hi = len(arr) (one past the end). If the search finishes with lo == len(arr), no True was found. If lo == 0, the first element is True. This sentinel approach avoids special-case checks after the loop.
Three boundary scenarios: (1) is false everywhere — return as a sentinel; (2) is true everywhere — return ; (3) array is empty — return without any probes. All three are handled automatically if you initialise (half-open) and maintain the loop condition — the final value of is the first true index or .
Find the first True in [False, False, False, False] (all False) and in [True, True, True] (all True).
Related Problems on PixelBank
A shipping company must move a fixed list of packages, in order, within D days, and wants to rent the smallest ship that can do it. There is no array of capacities to search, and trying every capacity from 1 upwards could mean millions of simulations. Yet one fact makes this easy: if a capacity works, every larger capacity also works.
The previous topic, Sorted Boolean Array, found the first True in an array of booleans. This topic removes the array entirely. Any function over an ordered range of candidate answers that switches once from False to True, such as "can we ship everything in D days with capacity x?", is a sorted boolean array computed on demand. Binary searching over the candidate values x is called binary search on the answer, and it costs O(log(range)) calls to the check.
We first learn to recognize the pattern and set the search bounds, then focus on the hard part, designing a fast and provably monotonic feasibility check, usually a greedy scan. Many optimization problems phrased "minimize the maximum" or "maximize the minimum" fall to this recipe.
Definition
Binary search on the answer applies binary search to a range of candidate answers [lo, hi] instead of an array. It requires a feasibility predicate feasible(x) that is monotonic: once True, it stays True for every larger x in a minimization problem. The smallest feasible x is found with O(log(hi - lo)) evaluations of the predicate.
In this topic
- 1Binary Search on Answer Space
- 2Designing the Feasibility Check
Binary Search on Answer Space
Instead of searching an array, you search a range of possible answers. Define a function feasible(x) that returns True if x is a valid answer and False otherwise. If feasible is monotonic (once it becomes True, it stays True for all larger x), then you can binary search on x to find the smallest feasible answer. This technique is sometimes called 'binary search on the answer' and it turns many hard optimization problems into simple feasibility checks.
When a feasibility function is monotone in the answer — e.g., 'can we finish in hours?' — the set of feasible answers is a suffix of the real line. Binary search on converges to the smallest feasible in where is the cost of one feasibility check. This collapses optimisation to search.
A shipping company needs to deliver packages with weights [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] in 5 days. What is the minimum ship capacity needed?
Designing the Feasibility Check
The hardest part of binary search on the answer is writing the feasible(x) function. It must be efficient (ideally O(n) or O(n log n)) since it is called O(log(range)) times. The function must also be monotonic: if feasible(x) is True, then feasible(x + 1) must also be True (for minimization problems). Common feasibility checks involve greedy algorithms --- scan through the data and check if the constraint can be satisfied with the given answer.
For the check to work, it must be strictly monotone (or at least non-decreasing). Example (Koko bananas): 'can she finish at rate in hours?' reduces to . Because is non-increasing in , is monotone: if rate works, so does any higher rate — exactly the precondition binary search requires.
Given an array [7, 2, 5, 10, 8] and k=2 groups, find the minimum possible maximum sum of a group (minimize the largest subarray sum).
A leaderboard holds a million sorted scores and you must answer, over and over, how many players scored at least 80, or where a new score of 80 should be inserted to keep the list sorted. The score 80 may appear many times or not at all, so a search that returns "found at some index" is not good enough. You need a precise position: the first slot whose value is at least 80.
The previous topic, Monotonic Function, taught that binary search really finds the boundary of a monotonic predicate. Applying that view to a sorted array with the predicate "arr[i] >= target" gives the lower bound, also known as bisect_left in Python or std::lower_bound in C++. It always returns a valid insertion point, handles duplicates without special cases, and never needs to test for equality.
We first build the lower-bound template and trace it on duplicates. Then we add its twin, the upper bound, the first element strictly greater than the target, and combine the two to count occurrences and answer range queries in O(log n), which is the backbone of sorted-data indexes.
Definition
The lower bound of a target in a sorted array is the smallest index i with arr[i] >= target, or n if no such index exists; it equals the leftmost insertion point that keeps the array sorted. The upper bound is the smallest index with arr[i] > target. Both are found by binary search in O(log n) comparisons.
In this topic
- 1Lower Bound Binary Search
- 2Upper Bound and Range Queries
Lower Bound Binary Search
The lower bound search finds the leftmost position where target could be inserted without breaking the sorted order. If the target exists in the array, it returns the index of its first occurrence. If it does not exist, it returns the index where it would be inserted. The key difference from standard binary search: when arr[mid] == target, we do NOT return immediately. Instead, we set hi = mid to keep searching leftward for an earlier occurrence.
returns the smallest index with . It is the transition index of the predicate , which is monotone on a sorted array. Runtime is with comparisons. This single primitive implements equality search, range counts, and insertion points.
Find the lower bound of 5 in [1, 3, 5, 5, 5, 8, 10].
Upper Bound and Range Queries
Lower bound alone cannot tell you where a run of equal values ends, and counting or slicing a range needs both ends. The upper bound is the first index whose value is strictly greater than the target. Its code is identical to lower bound except the predicate changes from arr[mid] >= target to arr[mid] > target. Together they bracket every copy: the target occupies indices lower_bound(t) through upper_bound(t) - 1, so its count is upper_bound(t) - lower_bound(t), with zero meaning absent. For a value range [a, b], the matching slice runs from lower_bound(a) to upper_bound(b). Python exposes these as bisect_left and bisect_right.
returns the smallest with . Combined with , the count of elements equal to is , and the count in is — all .
Count how many times 5 appears in [1, 3, 5, 5, 5, 8, 10] using lower and upper bound.
A server log is sorted by timestamp, and an incident began at 12:03:07. You need the very first log line from that second, because later lines at the same timestamp are symptoms, not the cause. A plain binary search happily reports whichever matching line it hits first, which is usually somewhere in the middle of the run.
The previous topic, First Element Not Smaller, solved a close cousin with lower bound, which returns an insertion point even when the target is absent. First occurrence adds one extra rule: the target must actually exist, otherwise return -1. More importantly, it teaches the most common binary-search correction in interviews. When arr[mid] == target, you are not done; you have a candidate, and there may be an earlier match to its left.
We first write the "record and keep searching" version that remembers the best index seen so far while continuing leftward. Then we mirror it to find the last occurrence, and combine the two to get the full range and count of a value in O(log n), regardless of how many duplicates there are.
Definition
The first occurrence of a target in a sorted array is the smallest index i with arr[i] == target, or -1 if the target is absent; the last occurrence is the largest such index. Both are found by binary search that records each match as a candidate and keeps narrowing toward the desired side, in O(log n) time.
In this topic
- 1Searching Past a Match
- 2Last Occurrence Variant
Searching Past a Match
In standard binary search, finding arr[mid] == target means you are done. In first-occurrence search, finding a match means you have a candidate answer, but there might be an earlier one. The strategy: when arr[mid] == target, save mid as the current best answer and set hi = mid - 1 to continue searching the left half. The loop still runs with lo <= hi. After the loop, return the saved answer (or -1 if no match was found).
Naïve binary search returns some match. To find the first match, change the update rule: when , set (don't stop) instead of returning. This keeps searching the left half. Correctness: the invariant 'the first occurrence, if any, lies in ' is preserved because any occurrence to the right of cannot be the first. Still .
Find the first occurrence of 5 in [1, 3, 5, 5, 5, 7, 9].
Last Occurrence Variant
Finding the last occurrence is the mirror image of the first, and together they give the full extent of a value. The template is identical except for the direction of travel after a match: when arr[mid] == target, save mid as the candidate and set lo = mid + 1 to keep searching right; the other branches stay unchanged. With first and last in hand, the value occupies indices [first, last] and its count is last - first + 1. The tempting shortcut, finding one match and walking outward, costs O(k) for k duplicates and degrades to O(n). Running two separate O(log n) searches is always safe.
Symmetrically, to find the last occurrence, on match set and remember as the best-so-far. Equivalently, when that index still equals . Counting duplicates then takes two binary searches for time instead of a linear scan.
Find both the first and last occurrence of 3 in [1, 2, 3, 3, 3, 3, 4, 5].
Your code needs the square root of 2 to ten decimal places, or the interest rate that makes a loan's payments exactly match a target, and no library function gives it directly. What you do have is a way to check any guess: square it, or simulate the payments, and see whether you went too high or too low.
The previous topic, First Occurrence, worked on discrete indices, where each step could skip past mid with mid + 1. Here the candidates are real numbers, so there is no next index to step to. Instead, we keep an interval [lo, hi] that brackets the answer and halve its width every step, stopping once it is narrower than the precision we need. Because increases with x, the too-high or too-low verdict at the midpoint tells us which half to keep. This is the bisection method.
We first adapt the loop to continuous space, with a new stopping rule and no plus-one updates. Then we compute exactly how many iterations a given precision costs, and see why a fixed iteration count is safer than an epsilon test in floating point.
Definition
Binary search on real numbers, also called the bisection method, finds a value x where a monotonic function crosses a threshold by repeatedly halving an interval [lo, hi] known to contain it. After k iterations the interval width is (hi - lo) / 2^k, so reaching precision epsilon takes about log2((hi - lo) / epsilon) iterations.
In this topic
- 1Binary Search on Real Numbers
- 2Iteration Count and Convergence
Binary Search on Real Numbers
When searching over real numbers, the loop condition changes from lo <= hi (discrete) to hi - lo > epsilon (continuous), where epsilon is the desired precision (e.g., 1e-9). At each step, mid = (lo + hi) / 2.0. If mid * mid < target, the square root is larger, so lo = mid. If mid * mid > target, the square root is smaller, so hi = mid. Note: we use lo = mid (not mid + 1) because there is no 'next' value in continuous space.
For continuous and monotone, bisection converges linearly: after iterations, the interval width is . To guarantee error , we need iterations — independent of any . For double precision () on , this is only iterations.
Estimate sqrt(2) to 4 decimal places using binary search.
Iteration Count and Convergence
Before running a real-valued search, you can predict its cost exactly. Each iteration halves the interval, so after k steps its width is R / 2^k, where R = hi - lo is the starting range. Requiring width below epsilon gives , logarithmic in both range and precision: [0, 10^9] at 10^-9 needs about 60 iterations. The failure mode is floating point. A double carries about 16 significant digits, so when epsilon is smaller than the gap between adjacent doubles near the answer, hi - lo > eps never becomes false and the loop spins forever. A fixed count such as 100 iterations always terminates.
Bisection's error after steps is , so convergence is linear with rate (one bit of precision per iteration). Newton's method achieves quadratic convergence on smooth functions, but bisection's advantage is robustness: it never diverges if the initial bracket contains a root, making it the method of choice when you care about guarantees over speed.
How many iterations does binary search need to estimate sqrt(1000000) with precision 10^-10?
A circular log buffer stores events in time order, but writing wrapped around, so the array now reads [40, 50, 60, 10, 20, 30] and the oldest entry sits somewhere in the middle. You need its position quickly, without scanning the whole buffer. The array is no longer sorted, so it seems the tools of this chapter no longer apply.
The previous topic, Square Root Estimation, searched a smooth monotonic function. A rotated sorted array is not monotonic, but it is almost: it consists of two sorted runs, and the minimum sits exactly at the break between them. The key insight is that comparing the middle element with the rightmost element reveals which side of the break mid is on, which is again a yes/no predicate that flips exactly once. So binary search still finds the minimum in O(log n).
We first derive the rule for identifying the sorted half and see why comparing with arr[hi] rather than arr[lo] also handles an unrotated array. Then we add duplicates, which can make the comparison ambiguous and, in the worst case, force a linear-time fallback.
Definition
A rotated sorted array is a sorted array whose first k elements have been moved to the end, forming two ascending runs; the minimum sits at the start of the second run. With distinct values, binary search comparing arr[mid] against arr[hi] locates it in O(log n). With duplicates, the worst case degrades to O(n).
In this topic
- 1Identifying the Sorted Half
- 2Handling Duplicates
Identifying the Sorted Half
In a rotated sorted array, at least one half (left or right of mid) is always properly sorted. Compare arr[mid] with arr[hi]: if arr[mid] > arr[hi], the rotation point (minimum) is in the right half, so lo = mid + 1. If arr[mid] <= arr[hi], the right half is sorted and the minimum is either at mid or in the left half, so hi = mid. This comparison with arr[hi] (not arr[lo]) is essential because it correctly handles the case when the array is not rotated at all.
In a rotated sorted array, at every iteration at least one of and is fully sorted — recognisable by comparing endpoints: means the left half is sorted. Once you know which half is sorted, you can decide in whether the target lies there (using the sorted-half's endpoints) and recurse. This preserves the bound despite the rotation.
Find the minimum in [4, 5, 6, 7, 0, 1, 2].
Handling Duplicates
When the array can contain duplicates, arr[mid] == arr[hi] creates ambiguity --- you cannot determine which half to search. The solution is to shrink hi by 1: hi -= 1. This safely eliminates one element (since arr[mid] has the same value) without missing the minimum. However, this worst-case scenario (e.g., [1, 1, 1, 1, 0, 1, 1]) degrades the time complexity from O(log n) to O(n), because you might only eliminate one element per iteration.
With duplicates, no longer tells you which half is sorted — e.g., defeats the standard argument. The fix is to shrink the range: when , increment (or decrement ). This degrades worst-case complexity to — e.g., an all-equal array with one different value — but remains on average.
Find the minimum in [2, 2, 2, 0, 1, 2] (array with duplicates).
Related Problems on PixelBank
A sensor reading climbs steadily, peaks, then falls, and you must find the moment of maximum signal among millions of samples. The data is not sorted, so neither plain binary search nor lower bound applies directly, but reading every sample wastes time on a question whose answer the shape almost gives away.
The previous topic, Minimum in Rotated Array, showed that binary search works whenever a single comparison at mid reveals which side holds the answer. A mountain array, one that strictly increases to a peak and then strictly decreases, has exactly that property. Comparing arr[mid] with its right neighbour tells you whether mid is on the rising or falling slope: rising means the peak is to the right, falling means it is at mid or to the left. The predicate "is mid on the falling slope" is again False then True, so the peak is found in O(log n).
We first implement the slope comparison and see why mid + 1 is always in bounds. Then we examine plateaus of equal values and arrays with multiple local peaks, where the slope carries less information and the guarantees weaken.
Definition
A mountain array strictly increases up to a single peak index p and strictly decreases after it. Finding p is a binary search on the predicate arr[i] > arr[i + 1], which is False before p and True from p onwards. More generally, in any array with no equal neighbours, the same search returns some local peak in O(log n).
In this topic
- 1Using the Slope to Search
- 2Plateau and Multiple Peaks
Using the Slope to Search
At any index mid, compare arr[mid] with arr[mid + 1]. If arr[mid] < arr[mid + 1], you are on the ascending slope --- the peak is to the right, so lo = mid + 1. If arr[mid] > arr[mid + 1], you are on the descending slope --- the peak is at mid or to the left, so hi = mid. The loop runs with while lo < hi and converges to the peak index when lo == hi.
In a mountain (unimodal) array, the peak is the unique index where . At each midpoint , the local slope tells you which side the peak is on: positive slope ⇒ peak in , negative slope ⇒ peak in . This reduces the range by half each iteration, giving . The invariant is 'a peak always lies in ', provable by induction using the endpoint conditions.
Find the peak in [1, 3, 5, 8, 6, 4, 2].
Plateau and Multiple Peaks
The slope rule relies on arr[mid] and arr[mid + 1] differing. On a plateau, where they are equal, the comparison says nothing about where the peak lies, and moving either way can skip it: in [1, 2, 2, 2, 2, 3, 1], choosing the left side when equal ends on a flat 2 and misses the peak 3 at index 5. Binary search is therefore only guaranteed when adjacent elements differ, which is why the classic Find Peak Element problem forbids equal neighbours; with plateaus, the safe fallback is an O(n) scan. With multiple strict local peaks, the search returns some local peak, not necessarily the global maximum.
If equal values form plateaus, the slope test is uninformative and you must scan past the plateau — worst case when the whole array is constant. For the Find Peak Element problem where is guaranteed, the slope test is always decisive and is restored. Multiple peaks are fine — binary search finds any one of them.
Find a peak in [1, 3, 5, 5, 4, 2] where there is a plateau at indices 2 and 3.