Binary Search

Binary search isn't just looking up a value in a sorted array — the real skill is spotting a monotone predicate hidden in the problem and searching on the answer spaceinstead of an index. When you can ask “is X feasible?” and the answer flips exactly once from false to true, you can binary-search it in O(log n).

Topic guide10 problems
The unlock

The real subject of binary search is not a sorted array — it is a monotone yes/no question. If “is X feasible?” flips from no…no…yes…yes exactly once as X grows, you can bisect straight to the boundary in O(log n), throwing away the half that provablycan't hold the answer at every step.

MENTAL MODEL You are hunting one boundary, not a value

Forget “look up a number in a sorted list” — that's one special case. The general picture is a line of yes/no answers that is sorted by truth: F F F F | T T T. There is exactly one seam where false becomes true, and binary search is just a fast way to find that seam.

Each probe asks the question at the midpoint. The answer doesn't just give you one data point — because the line is monotone, it tells you which entire half the seam cannot be in, so you delete that half. A sorted array is simply the case where the question is arr[i] ≥ target.

The reframe →don't ask “where is the value?” Ask “what monotone yes/no predicate p(x)is this problem really about, and where does it first turn true?”

SEE IT lo / mid / hi collapsing — on a real array and on an imaginary one

Classic search first. Watch lo, mid, hi squeeze in, and watch a whole half vanish on every probe:

looking for 23 in a sorted array

 idx :   0    1    2    3    4    5    6    7
 val : [ 3 ][ 9 ][12 ][17 ][23 ][28 ][41 ][55]
        lo            mid                  hi      arr[mid]=17 < 23 → go RIGHT
                              lo   mid     hi      arr[mid]=41 > 23 → go LEFT
                              lo,hi                arr[mid]=23  ✓  found, idx 4
        ^^^^^^^^^^^^^^^^^^^^                       half thrown away EACH probe
        provably can't hold 23 → never looked at again

3 probes, not 8.  Each guess deletes a provable-empty half.

Now the same loop with no array at all. For Koko eating bananas the “array” is the line of speeds, and the comparison is the feasibility question. We binary-search the F | T boundary:

Koko: smallest eating speed k that finishes the bananas in time.

  p(k) = "can Koko finish at speed k?"   (monotone: slow=no, fast=yes)

  speed k :  1    2    3    4    5    6    7    8
  p(k)    :  F    F    F    F    T    T    T    T
                            ↑    ↑
                            |    └── first TRUE  ← THIS is the answer
                            └─────── last  FALSE

  There is no array. We INVENT the index line [1 .. max(pile)]
  and binary-search it for the F|T boundary, calling p(mid) as the
  "comparison". Same loop, the array is imaginary.
The smell test → if you can plot the candidate answers on a line and color each one false or true, and the colors never re-mix (all F's then all T's), you can binary-search the boundary — array or not.

HOW TO THINK The cold-start ladder — run this on any new problem

When a problem smells like “find the best/smallest/largest X” and brute force would just try every X, climb these rungs before writing a loop:

  1. Phrase the goal as a yes/no question.Turn “minimum speed to finish in time” into p(k) = can we finish at speed k?” The answer you want is a boundary between no and yes.
  2. Check monotonicity. If p(x) is true, is it still true for every larger x (a bigger speed, more capacity, more days)? If yes, p goes false → true exactly once. No single flip → no binary search.
  3. Pin the bounds. lo = smallest candidate that could be the answer, hi= a value you know is feasible. Ask “what unitare these in?” — indices, or speeds/capacities (the answer space)?
  4. Bisect for the first true. Probe the midpoint, call p(mid), keep the half that still contains the seam. The boundary is your answer.
The one trick → the hard part is spotting the predicate, not writing the loop. “Minimize the maximum”, “maximize the minimum”, and “smallest X that works” are all neon signs for binary-search-on-the-answer.

SAY IT Name your invariant and the off-by-one fear dies

Almost all binary-search anxiety is off-by-one panic: or <? mid or mid+1? Cure it by stating one invariant out loud and never breaking it: the answer, if it exists, is always inside [lo, hi]. Every boundary update is then forced:

INVARIANT: the answer, if it exists, is always inside [lo, hi].
           every line below must keep that promise.

  mid ruled OUT   →  move the boundary PAST mid  (lo = mid+1 / hi = mid-1)
  mid might be IT →  KEEP mid in the window       (hi = mid)

  found-it template          lower_bound template
  while (lo <= hi)           while (lo < hi)
    ==  → return mid           p(mid) true  → hi = mid     (keep mid)
    <   → lo = mid+1           p(mid) false → lo = mid+1   (drop mid)
    >   → hi = mid-1
  closed [lo,hi], empties    half-open, collapses to the
  when lo > hi               single first-true position
  • If mid is ruled out, move the boundary past it (lo = mid+1 or hi = mid-1) — it can never be the answer, so dropping it keeps the invariant.
  • If mid might be it, you must keep it: hi = mid. Writing hi = mid - 1 here would throw away the very answer.
Why mid = lo + (hi - lo) / 2 it's the same midpoint as (lo + hi) / 2 but can't overflow, and it rounds down — so in the lower_bound template mid never equals hi, which is exactly what stops the two-element infinite loop.

TEMPLATE SHAPE One skeleton: search for the first true

You don't need four memorized templates — you need one and the discipline to phrase the question as a predicate. Read this as a sentence: while more than one candidate survives, probe the middle and keep the half holding the seam.

find_first_true(lo, hi):             # search for the F|T boundary
                                     # invariant: answer in [lo, hi]

    while lo < hi:                   # window still has >1 candidate
        mid = lo + (hi - lo) / 2     # midpoint, no overflow

        if p(mid):                   # mid satisfies the predicate
            hi = mid                 #   → mid might be the answer, KEEP it
        else:                        # mid fails
            lo = mid + 1             #   → answer is strictly to the right

    return lo                        # lo == hi: the one surviving candidate

The plain “found-it” template (lo ≤ hi, return on equality) is the special case where the predicate is arr[i] === target and you can stop early. The lower_bound shape above is the one that alwaysworks — exact match, first/last position, and binary-search-on-answer all collapse into “find the first x where p(x)is true.”

WHEN IT BREAKS No single flip means no boundary to find

Binary search silently returns garbage the moment the predicate stops being monotone. The whole trick rests on one promise: once true, always true (or once false, always false). If p(x) reads F T F T across the range, throwing away a half is no longer safe — the seam you deleted might have held the answer.

  • The good case: “capacity cships everything in Ddays” — more capacity is never worse, so feasibility only ever turns on. Bisect away.
  • The trap: “is there a peak at index x?” across a random array — true in scattered spots, not a clean F…T line. (Peak-finding works only because of a different monotone argument about slopes, not because the array is sorted.)
Before you bisect → prove the flip happens once. Ask “if x works, does every larger x work too?” If you can't say yes, you don't have a binary-search problem yet — go find the real monotone quantity.

MNEMONIC Kill half every guess.

Kill half every guess.On a monotone space, one probe at the midpoint tells you which half can't contain the answer — throw it away. log₂(n) probes total. The Visualize tab greys out the discarded half each step.

PATTERN Halving a sorted search space

Binary search works whenever the search space can be ordered and each midpoint comparison permanently eliminates one half. The classic case is a sorted array: if arr[mid] < target you discard everything to the left, cutting work in half with every step.

The loop invariant is the core idea: at every iteration, the answer (if it exists) lies within [lo, hi]. Every update must preserve that invariant. The loop terminates when the window shrinks to zero or one element.

Golden rule →never move a boundary past a position that could still hold the answer. That's why you use hi = mid (not mid - 1) in lower-bound variants when mid itself might be the answer.

KEY IDEA The real skill: spotting the monotone predicate

Most interview binary-search problems don't hand you a sorted array — they give you a condition feasible(x) that is monotone: false for all small values of x and truefor all large ones (or vice versa). You never see the “sorted array” — you construct the sorted space from the problem constraints.

  • Koko eating bananas: “can Koko eat all bananas at speed k?” — false for tiny k, true for large. Binary-search k.
  • Ship packages in D days: “is capacity cenough?” — same shape.

Once you see the predicate, the algorithm is mechanical: search the answer range [lo, hi] for the first true.

FRAMING Lower-bound / first-true framing

Unify all variants under one mental model: you are always looking for the first position where a predicate becomes true. For exact search the predicate is arr[i] >= target; for a feasibility search it's feasible(i). The lower-bound template (while lo < hi, hi = mid on true) handles every case cleanly.

COST What it buys you

Linear scan
O(n)
Check every element or every candidate answer.
Binary search
O(log n)
Halve the space each step. With O(n) feasibility check: O(n log n).

When the feasibility check is itself O(n) (e.g. simulate a greedy), the overall cost is O(n log(hi−lo)) — still dramatically better than a brute-force scan of every candidate.

RUN IT Kill half every guess

step 0 / 4
STARTSearch for 11 in a sorted array. Window [lo, hi] covers everything. Kill half every guess.
sorted1031527394115136157
State
0
lo
mid
7
hi
11
target
mid (probe)lo / hi boundsdiscarded half
slowfast

TRIGGERS When you see ___ → reach for ___

Reach for binary search whenever the problem has a sorted structure or a monotone condition you can evaluate at any candidate value — especially when an O(log n) or O(n log n) bound is expected.

"sorted array" + find / insert position / countclassic exact-match or lower-bound search
"find the minimum/maximum value such that f(x) is feasible"binary search on the answer with a feasibility function
"minimize the maximum" or "maximize the minimum"binary search on the answer — the optimal value has a monotone feasibility boundary
"rotated sorted array" — search or find minimumdetermine which half is cleanly sorted, then narrow
O(log n) required and input has some ordering or monotone propertybinary search on index or on the value/answer range
"Koko / ship packages / split array" — can we do it within a limit?binary search on the answer; write a greedy feasible() check
"find the first / last occurrence" or "insert position" in sorted datalower-bound template (while lo < hi, hi = mid)

RED FLAGSWhen it's NOT this pattern

  • Unsorted with no monotone predicate. Binary search requires that every midpoint decision permanently eliminates one half. On a random array there is no such guarantee — use a hash map or linear scan.
  • You need all occurrences, not just one position. Binary search finds a boundary in O(log n), but collecting every matching element is still O(k). If k can be large, consider a different structure.
  • Tiny input (n ≤ 20). A linear scan is simpler and the constant factor of binary search adds cognitive overhead for no meaningful gain.
  • The predicate toggles more than once. If f(x) is true-false-true across the range, there is no single boundary to find — binary search will silently return the wrong answer.

TEMPLATE Classic exact-match search

When → The array is sorted and you want the index of an exact value (or -1 if absent). Use lo ≤ hi so the single-element window is still checked.

classic-exact-match-search.ts
function binarySearch(arr: number[], target: number): number {
  let lo = 0, hi = arr.length - 1;

  while (lo <= hi) {                           // search space: [lo, hi] (inclusive)
    const mid = lo + ((hi - lo) >> 1);         // avoids integer overflow
    if (arr[mid] === target) return mid;        // found
    if (arr[mid] < target)   lo = mid + 1;     // target is to the right
    else                     hi = mid - 1;     // target is to the left
  }
  return -1;                                   // not found
}
Loop condition lo ≤ hi the search space is the closed interval [lo, hi]. When lo passes hi the interval is empty and the target does not exist.

TEMPLATE Lower-bound / first-true predicate search

When → You want the first index (or first value) where a monotone predicate becomes true: first element ≥ target, first feasible answer, insert position, etc. Use while lo < hi with hi = mid to keep the answer candidate in range.

lower-bound-first-true-predicate-search.ts
// "First index where predicate(arr[i]) is true."
// Predicate must be monotone: false…false…TRUE…TRUE (no re-toggling).
function lowerBound(arr: number[], target: number): number {
  let lo = 0, hi = arr.length;                 // hi = arr.length (one past end)

  while (lo < hi) {                            // invariant: answer ∈ [lo, hi]
    const mid = lo + ((hi - lo) >> 1);
    if (arr[mid] < target) lo = mid + 1;       // mid is definitely NOT the answer
    else                   hi = mid;           // mid might be the answer, keep it
  }
  return lo;                                   // lo === hi, the first true position
}
Why hi = arr.length (not length - 1) → if every element is smaller than target, the answer is “one past the end” (insert at the tail). Setting hi = arr.length lets the algorithm express that naturally without a special-case guard.

TEMPLATE Binary search on the answer

When → The problem asks for the minimum (or maximum) value satisfying a condition — not an index in an array. Define a feasible(mid) check and binary-search the value range [lo, hi] directly.

binary-search-on-the-answer.ts
// Binary-search on the ANSWER (a value range, not an index).
// Use when: "find min/max X such that feasible(X) is true/false."
function minFeasible(lo: number, hi: number): number {
  // lo = smallest candidate answer, hi = largest candidate answer
  while (lo < hi) {
    const mid = lo + ((hi - lo) >> 1);
    if (feasible(mid)) hi = mid;               // mid works → try smaller
    else               lo = mid + 1;           // mid too small → must go larger
  }
  return lo;                                   // smallest value where feasible is true
}

// feasible() runs in O(n) → overall O(n log(hi - lo))
function feasible(mid: number): boolean {
  // problem-specific check: e.g., "can we do the task in mid days?"
  return true; // replace with real logic
}
Setting the bounds → lo is the smallest possible answer (often 1 or max(arr)), hi is the largest (often sum(arr) or a problem constraint). Tighten them to reduce iterations.

TEMPLATE Rotated sorted array search

When → The array was sorted then rotated at an unknown pivot. One of the two halves around mid is always cleanly sorted — use that to decide which side contains the target.

rotated-sorted-array-search.ts
// Search in a rotated sorted array (no duplicates).
function searchRotated(arr: number[], target: number): number {
  let lo = 0, hi = arr.length - 1;

  while (lo <= hi) {
    const mid = lo + ((hi - lo) >> 1);
    if (arr[mid] === target) return mid;

    // Figure out which half is cleanly sorted.
    if (arr[lo] <= arr[mid]) {                 // left half is sorted
      if (arr[lo] <= target && target < arr[mid]) hi = mid - 1;  // target in left
      else                                         lo = mid + 1; // target in right
    } else {                                   // right half is sorted
      if (arr[mid] < target && target <= arr[hi]) lo = mid + 1;  // target in right
      else                                         hi = mid - 1; // target in left
    }
  }
  return -1;
}
Key observation → compare arr[lo] to arr[mid] (not to target) to determine which half is sorted. Then check whether target falls inside the sorted half; if yes, discard the other side.

PITFALL Infinite loop from wrong boundary update

The most common bug: using lo = mid instead of lo = mid + 1 in the branch where mid is definitely not the answer. When lo === hi - 1, mid === lo, and if you set lo = mid again, nothing changes — infinite loop.

Rule: if the current mid is ruled out, always move the boundary past it (mid + 1 or mid - 1). Only use hi = mid when mid itself might still be the answer.

PITFALL <= vs < loop condition mismatch

Use while (lo <= hi) for exact-match (closed interval — shrinks to empty when lo > hi) and while (lo < hi) for lower-bound / first-true (half-open — converges to a single candidate). Mixing them either skips the last candidate or loops forever at a two-element window.

PITFALL Integer overflow in mid calculation

Writing mid = (lo + hi) / 2 overflows in languages with fixed-width integers when lo + hi > MAX_INT. The safe form is lo + ((hi - lo) >> 1) (or lo + Math.floor((hi - lo) / 2) in TypeScript/JavaScript where numbers are 64-bit floats — overflow is rare but the idiom signals intent and is worth habituating).

PITFALL Confusing "search space = index" vs "search space = answer value"

In classic array search, lo and hi are indices. In binary-search-on-answer problems they are values (e.g., eating speeds, capacities). If you mix the two framings — say, initialising hi = arr.length - 1 when it should be sum(arr)— the search range is wrong and the algorithm silently produces a bad answer. Always ask: “what unit are my bounds in?”

PROBLEMS

#153Find Minimum in Rotated Sorted Array#153 (Medium) — compare arr[mid] to arr[hi]: if arr[mid] > arr[hi] the minimum is in the right half, otherwise left (including mid).#33Search in Rotated Sorted Array#33 (Medium) — determine which half is cleanly sorted by comparing arr[lo] to arr[mid], then decide which half to discard.#704Binary Search#704 (Easy) — the canonical exact-match template; use it to cement lo ≤ hi and mid ± 1 muscle memory.#74Search a 2D Matrix#74 (Medium) — treat the m×n matrix as one flattened sorted array of length m×n; map index i → row i/n, col i%n.#875Koko Eating Bananas#875 (Medium) — binary search on the answer (eating speed k ∈ [1, max(piles)]); feasible(k) sums up ceil(pile/k) and checks ≤ h.#981Time Based Key-Value Store#981 (Medium) — each key maps to a sorted list of (timestamp, value) pairs; lower-bound search for the latest timestamp ≤ query.#4Median of Two Sorted Arrays#4 (Hard) — binary search the partition of the smaller array; ensure left halves of both arrays together contain ⌊(m+n)/2⌋ elements, then check the cross-boundary max/min.#162Find Peak ElementLocate any peak (a value greater than both neighbors) in O(log n) by binary searching on the slope: when nums[mid] < nums[mid+1] climb right, otherwise keep mid or go left — a peak always exists with -∞ sentinels at the ends.#540Single Element in a Sorted ArrayEvery element appears twice except one; find it in O(log n) by binary searching on pair parity — before the loner the first of each pair sits on an even index, after it the alignment shifts.#69Sqrt(x)Compute the integer square root of x without a sqrt call by binary searching the answer in [0, x], keeping the largest mid whose square does not exceed x (using mid ≤ x/mid to dodge overflow). O(log x).