E ExamMaster

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

  1. Pick midIn the current [low, high] range, read the middle index.
  2. CompareEqual ⇒ found. Target larger ⇒ search the right half. Target smaller ⇒ search the left half.
  3. 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 -1

Find 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?
  1. O(n)
  2. O(\log n)
  3. 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

  1. Same mathlow + (high − low)/2 equals (low + high)/2 for real numbers.
  2. Smaller sumhigh − low is at most the array length in index space; low + that half stays inside representable bounds more often.
  3. 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?
  1. It runs in O(1) while the sum form is O(\log n)
  2. It avoids overflowing the sum low + high in fixed-width integer arithmetic
  3. 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

  1. Rangelow and high are both valid indices (or high starts at n−1).
  2. Loopwhile low ≤ high — empty range is low > high.
  3. Discard midOn move right, low = mid + 1. On move left, high = mid − 1. Never leave mid in the range after rejecting it.
Two consistent conventions
StyleLoopGo leftGo right
Inclusive [low, high]low ≤ highhigh = mid − 1low = mid + 1
Half-open [low, high)low < highhigh = midlow = mid + 1
Using while low ≤ high with inclusive bounds, after rejecting nums[mid] as too small you set
  1. high = mid (leaving mid in the range)
  2. low = mid + 1 (discarding mid)
  3. 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)

  1. CandidateKeep an answer index, initially n ("not found / insert at end").
  2. Mid ≥ targetRecord mid as a candidate and search left (high = mid − 1).
  3. 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
  1. Any index where nums[i] equals target
  2. The first index i with nums[i] ≥ target
  3. 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

  1. Boundslow = smallest conceivable answer; high = largest (often a max input).
  2. Check midIf feasible(mid), record mid and search left for something smaller.
  3. 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
  1. A sorted array of candidate answers stored in memory
  2. A monotonic feasibility predicate on the answer value
  3. 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

  1. Name the orderIs the array sorted, or is the predicate on the answer monotonic?
  2. If neitherDo not halve. Scan, hash, or sort deliberately and own the cost.
  3. 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?
  1. Sort then binary search — always faster
  2. Linear scan in O(n), unless you will query many times after one sort
  3. 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

  1. IterativeTwo or three index variables on the stack frame of one function — constant space.
  2. RecursiveEach call waits on a half; depth is the number of halvings ≈ log n.
  3. Time unchangedBoth are O(\log n) comparisons on a sorted array.
Recursive binary search on n sorted elements uses how much extra stack space?
  1. O(1)
  2. O(\log n)
  3. 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)

  1. One stepPay O(1), leave a subproblem of size n/2.
  2. k stepsSize is n/2^k. Stop when that size is 1 ⇒ k ≈ log₂ n.
  3. 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
  1. O(n)
  2. O(\log n)
  3. 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.

Complexity sheet
VariantTimeExtra space
Classic iterative searchO(\log n)O(1)
Classic recursive searchO(\log n)O(\log n) stack
Lower / upper boundO(\log n)O(1)
Search on answerO(\log(\text{range}) \times C)O(1) plus check
Prerequisitesorted 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
Continue with Google — freeNo card, no trial. Works offline once installed.