Data Structures & Algorithms · Data Structures & Algorithms
Binary Search
Binary search on sorted data and on answer spaces for optimization problems.
Eight concepts on binary search — the halving loop, overflow-safe midpoints, inclusive bounds, lower and upper bound, search-on-answer with a monotonic predicate, and the sorted-array prerequisite that makes the whole family legal.
- Data Structures & Algorithms
- Medium level
- 8 concepts
- 5 practice questions
1Halve a sorted search space
On a sorted array, binary search compares the middle element to the target and discards half the remaining range each step. That is why the time is O(\log n): after about \lceil \log_2 n \rceil comparisons the range collapses to one cell. The recurrence is T(n) = T(n/2) + O(1).
Figure. First mid lands on 5; target 7 is larger, so the left half is discarded.
One comparison
- Pick midIn the current [low, high] range, read the middle index.
- CompareEqual ⇒ found. Target larger ⇒ search the right half. Target smaller ⇒ search the left half.
- ShrinkUpdate low or high so the discarded half never appears again.
Classic binary search
def binary_search(nums, target):
low, high = 0, len(nums) - 1
while low <= high:
mid = low + (high - low) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1Find 7 in [1, 3, 5, 7, 9, 11]
Sorted nums = [1, 3, 5, 7, 9, 11], target = 7. Return its index.
- low=0, high=5mid=2, value 5
- 5 < 7 ⇒ right halflow=3, high=5
- mid=4, value 99 > 7 ⇒ high=3
- mid=3, value 7index 3
Pro tip. A billion sorted elements need only about thirty comparisons — \log_2 10^9 \approx 30. Linear scan is the wrong instinct once the array is sorted.
Coding lab. Find 7 in the six-number row runs in the app, with checks on your output.
Binary search on a sorted array of size n takes how many comparisons in the worst case, to order of magnitude?
- O(n)
- O(\log n)
- O(1)
Each step halves the range, so the depth is logarithmic. Constant time would require direct indexing by value, which sorted arrays do not provide.
2Overflow-safe midpoint
The naive mid = (low + high) / 2 can overflow when low and high are large signed integers. Writing mid = low + (high − low) / 2 computes the same index using a difference that fits when the indices themselves fit. Same answer, fewer surprise bugs — a classic interview footgun.
Figure. mid = low + (high − low) / 2 is the same index as (low + high) / 2, but the sum low+high can pass the representable maximum while the half-difference stays between the endpoints.
Why the rewrite is safer
- Same mathlow + (high − low)/2 equals (low + high)/2 for real numbers.
- Smaller sumhigh − low is at most the array length in index space; low + that half stays inside representable bounds more often.
- HabitUse the safe form every time — it costs nothing when overflow is impossible and saves you when it is not.
Why prefer mid = low + (high − low) / 2 over (low + high) / 2?
- It runs in O(1) while the sum form is O(\log n)
- It avoids overflowing the sum low + high in fixed-width integer arithmetic
- It always rounds mid upward, which binary search requires
Both formulas pick the same mid in unbounded arithmetic. The rewritten form keeps intermediate values smaller so signed overflow is less likely.
3Bounds and loop conditions
Off-by-one bugs live in the loop condition and the mid update. Inclusive bounds with while low ≤ high pair with high = mid − 1 and low = mid + 1. Half-open [low, high) often uses while low < high and high = mid. Mixing the two conventions is how infinite loops and skipped cells appear. Pick one style and keep every update consistent with it.
Keep the Bounds and loop conditions table paired: inclusive low ≤ high with mid±1 updates, or half-open low < high with high = mid — never mix the two conventions.
Inclusive style checklist
- Rangelow and high are both valid indices (or high starts at n−1).
- Loopwhile low ≤ high — empty range is low > high.
- Discard midOn move right, low = mid + 1. On move left, high = mid − 1. Never leave mid in the range after rejecting it.
| Style | Loop | Go left | Go right |
|---|---|---|---|
| Inclusive [low, high] | low ≤ high | high = mid − 1 | low = mid + 1 |
| Half-open [low, high) | low < high | high = mid | low = mid + 1 |
Using while low ≤ high with inclusive bounds, after rejecting nums[mid] as too small you set
- high = mid (leaving mid in the range)
- low = mid + 1 (discarding mid)
- low = mid (risking an infinite loop when mid == low)
Mid was examined and rejected. It must leave the range. Setting low = mid when mid equals low does not shrink the range.
4Lower and upper bound
When duplicates exist, classic equality search returns any hit. Lower bound finds the first index whose value is ≥ target; upper bound finds the first index whose value is > target. Together they give the half-open range of equal values, still in O(\log n) — insertion points and frequency counts fall out of the same pair.
Figure. Lower bound of 2 is index 1; upper bound is index 4 (first > 2). The equal run is the bracketed triple.
Lower bound (first ≥ target)
- CandidateKeep an answer index, initially n ("not found / insert at end").
- Mid ≥ targetRecord mid as a candidate and search left (high = mid − 1).
- Mid < targetSearch right (low = mid + 1).
Lower bound of 2 in [1, 2, 2, 2, 4]
Find the first index of value ≥ 2 in nums = [1, 2, 2, 2, 4].
- low=0, high=4; mid=2 (value 2)≥ 2 ⇒ answer=2, high=1
- mid=0 (value 1)1 < 2 ⇒ low=1
- mid=1 (value 2)≥ 2 ⇒ answer=1, high=0
- low > highlower bound index 1
Pro tip. Upper bound uses the same skeleton with the test mid > target (strict). The equal run is [lower, upper).
Lower bound of target in a sorted array is
- Any index where nums[i] equals target
- The first index i with nums[i] ≥ target
- The last index i with nums[i] ≤ target
Lower bound is the leftmost feasible insertion point for target — first value at least as large as target.
5Binary search on the answer
Sometimes the array is not what you search — the answer value is. If a feasibility predicate is monotonic (once speed s works, every larger speed also works), binary-search the answer between a low and high bound and keep the extreme feasible value. Cost is O(\log(\text{range}) \times cost of one feasibility check). "Minimum capacity / speed / days that works" is the signature.
Figure. Binary search on the answer walks a numeric range where a predicate flips from false to true once — return the first true.
Minimize a feasible value
- Boundslow = smallest conceivable answer; high = largest (often a max input).
- Check midIf feasible(mid), record mid and search left for something smaller.
- InfeasibleSearch right — mid was too aggressive.
Koko eating bananas
Piles = [3, 6, 7, 11], h = 8 hours. Find the minimum integer eating speed so every pile finishes within h hours. Feasibility: total hours = sum of ceil(pile/speed) ≤ h. Search speeds in [1, max(pile)].
- speed 3: ceil(3/3)+ceil(6/3)+ceil(7/3)+ceil(11/3)1+2+3+4 = 10 > 8 — infeasible
- speed 4: ceil(3/4)+ceil(6/4)+ceil(7/4)+ceil(11/4)1+2+2+3 = 8 ≤ 8 — feasible
- Monotonic: feasible(4) ⇒ every s ≥ 4 worksdiscard speeds < 4; keep searching left of large s
- Minimum feasible speed4
Pro tip. If the predicate is not monotonic, binary search on the answer is illegal — you can discard the wrong half. Prove monotonicity before you code the loop.
Binary search on the answer requires
- A sorted array of candidate answers stored in memory
- A monotonic feasibility predicate on the answer value
- That the answer equal an array element
You are searching a numeric range, not an array index. Monotonicity of "does this value work?" is what lets you discard a half.
6Sorted or monotonic first
Plain binary search on indices assumes the array is sorted in the direction your comparisons expect. If it is not, either sort first (O(n \log n) — often slower than a linear scan for one query) or use a different structure. Search-on-answer replaces "sorted array" with "monotonic predicate," but something still has to be monotonic. Applying the halving loop to unsorted data is undefined behavior for correctness, not merely slow.
Figure. Binary search needs a sorted (or monotonic) space; an unsorted row makes every midpoint guess meaningless.
Before you binary-search
- Name the orderIs the array sorted, or is the predicate on the answer monotonic?
- If neitherDo not halve. Scan, hash, or sort deliberately and own the cost.
- If you sort for one queryCompare O(n \log n) sort-plus-search against O(n) linear — sorting rarely wins for a single lookup.
You need one lookup of a value in an unsorted array of size n. Best default?
- Sort then binary search — always faster
- Linear scan in O(n), unless you will query many times after one sort
- Binary search anyway; unsorted data still halves correctly
Sorting to enable one binary search costs O(n \log n), worse than one scan. Binary search on unsorted data can miss the target entirely.
7Iterative versus recursive space
The iterative loop uses O(1) extra space. A recursive formulation that binary-searches a half in a call uses O(\log n) stack space for the depth of the recursion. Same comparisons, different memory profile — prefer iterative unless the recursive shape makes the invariant clearer for a variant you are proving.
Figure. Same O(\log n) comparisons; recursion pays O(\log n) call-stack frames the iterative loop does not.
Space accounting
- IterativeTwo or three index variables on the stack frame of one function — constant space.
- RecursiveEach call waits on a half; depth is the number of halvings ≈ log n.
- Time unchangedBoth are O(\log n) comparisons on a sorted array.
Recursive binary search on n sorted elements uses how much extra stack space?
- O(1)
- O(\log n)
- O(n)
One frame per halving level. Iterative search keeps that in registers/locals instead of a call stack.
8Complexity and the recurrence
Binary search's time recurrence T(n) = T(n/2) + O(1) unfolds to O(\log n) because you pay constant work per level and there are \Theta(\log n) levels until n shrinks to 1. Search-on-answer replaces n with the width of the answer range: O(\log(\text{range}) \times C) when each feasibility check costs C. Memorize the shape; derive the log from the recurrence rather than treating it as a slogan.
Count the halvings on the Halve a sorted search space strip: each step cuts the live range in two, so n shrinks to 1 in \lfloor \log_2 n \rfloor probes.
Unrolling T(n)
- One stepPay O(1), leave a subproblem of size n/2.
- k stepsSize is n/2^k. Stop when that size is 1 ⇒ k ≈ log₂ n.
- Totalk constant-time steps ⇒ O(\log n).
How many halvings for n = 16?
Starting from a range of 16 elements, how many times can you halve before one element remains?
- 16 → 81 step
- 8 → 4 → 2 → 13 more steps
- total halvings4 = log₂ 16
Pro tip. Ceiling matters on odd lengths — the big-O statement absorbs it. Interviewers still like hearing \lceil \log_2 n \rceil as the iteration cap.
T(n) = T(n/2) + O(1) solves to
- O(n)
- O(\log n)
- O(n \log n)
Constant work per halving level, logarithmic levels. The n log n shape needs linear work per level (like mergesort).
Notes
- Core Idea: On a sorted array, repeatedly compare the middle element and discard half the search space, giving O(\log n) time.
- Overflow-Safe Midpoint: Compute mid = low + (high - low) / 2 to avoid integer overflow from (low + high).
- Boundary Correctness: Carefully choosing inclusive/exclusive bounds and the loop condition (low <= high vs low < high) prevents off-by-one and infinite loops.
- Binary Search on Answer: When a predicate is monotonic (feasible below a threshold, infeasible above), binary-search the answer value instead of an index.
- Lower/Upper Bound: Variants find the first element >= target (lower bound) or first > target (upper bound) for insertion positions and ranges.
Formulas
- Time complexity: O(\log n); space O(1) iterative, O(\log n) recursive.
- Recurrence: T(n) = T(n/2) + O(1), which solves to O(\log n).
- Max iterations: about \lceil \log_2 n \rceil steps to reduce n to 1.
- Binary search on answer: O(\log(\text{range}) \times \text{cost of feasibility check}).
- Prerequisite: the data (or predicate) must be sorted/monotonic.
Exam traps & shortcuts
- Only apply plain binary search when the array is sorted; otherwise sort first (O(n \log n)) or use a different method.
- For 'minimum capacity/speed/value that works' problems, binary-search the answer space using a monotonic feasibility check.
- Use mid = low + (high - low) / 2 to avoid overflow, a classic bug source.
- For duplicates or ranges, use lower-bound/upper-bound variants to find first/last occurrence in O(\log n).
Reference tables
Assumes a sorted array or a monotonic predicate. C is the cost of one feasibility check when searching on the answer.
| Variant | Time | Extra space |
|---|---|---|
| Classic iterative search | O(\log n) | O(1) |
| Classic recursive search | O(\log n) | O(\log n) stack |
| Lower / upper bound | O(\log n) | O(1) |
| Search on answer | O(\log(\text{range}) \times C) | O(1) plus check |
| Prerequisite | sorted data or monotonic predicate | — |
Recap
Binary search is discard-half under a monotonic order. Get the mid and bounds right, then notice when the "array" is really the answer space.
- Prerequisite
- Sorted (or monotonic predicate) first — else do not halve.
- Mid and bounds
- mid = low + (high − low) / 2; keep loop and updates in one convention.
- Bounds variants
- Lower bound = first ≥ target; upper = first > target.
- On the answer
- Minimize/maximize "what works" ⇒ search on answer.
- Complexity
- T(n)=T(n/2)+O(1) ⇒ O(\log n); recursive form spends stack.
Practise Binary Search
Reading is free and needs no account. Practice, mocks and progress live in the app.
- 5 exam-style questions on this topic, with explanations
- A 5-question practice set that ends the chapter
- Timed mocks scored with the real marking scheme
- Readiness tracked per topic, kept on your device