Why does a half-open lower bound search assign hi = mid rather than hi = mid - 1?
answer
- ask what mid means in each branch
- only one branch rules mid out
- you want the FIRST qualifying index
- trace a two-element array by hand
- a candidate must stay inside the range
basics
~20 sBecause mid may itself be the answer. A midpoint that satisfies the predicate is the earliest qualifying index found so far, so it must stay in range; mid - 1 discards it and the search returns a position too far left.
solid answer
~50 sThe loop maintains the invariant that the answer lies in the half-open range `[lo, hi)`: every index below `lo` fails the predicate `a[i] >= target`, and every index at or above `hi` satisfies it. When `a[mid] >= target`, `mid` is a *candidate* — possibly the very first qualifying index — so the range must shrink to `[lo, mid)` plus mid itself, which is exactly `hi = mid`. Writing `hi = mid - 1` throws away the candidate: on `[3, 5]` searching for 5, it returns 0 instead of 1. The other branch is asymmetric on purpose: when `a[mid] < target`, mid is definitively ruled out, so `lo = mid + 1` is safe and is also what guarantees progress. The loop ends when `lo == hi`, an empty range, and `lo` is then the answer — including `lo == n` when nothing qualifies.
code
pseudocode · 10 lines// a is sorted ascending; find the first index i with a[i] >= target
lo = 0
hi = length(a) // half-open search range [lo, hi)
while lo < hi:
mid = lo + (hi - lo) / 2 // integer division, rounds down
if a[mid] >= target:
hi = mid // mid still a candidate, keep it
else:
lo = mid + 1 // mid ruled out, discard it
return lo // equals length(a) if nothing qualifiesgo deeper
Focus on tracing the loop by hand on a two- or three-element array. Being able to say what lo and hi mean at each step is enough at this level.
State the invariant explicitly and use it to justify each branch, including why the failing side excludes its midpoint for termination while the passing side must retain it.
Demonstrate the diagnosis skill: this bug returns a plausible in-range index rather than crashing, so name the two-element case that exposes it and the property test that would have caught it.
Argue for standardising on one boundary template across a codebase rather than hand-rolling variants, and be able to justify the maintenance cost of a search everyone half-remembers.
## The invariant is the whole algorithm Every correct binary search is a loop invariant with some arithmetic attached. For the half-open boundary template the invariant is: > The first index satisfying the predicate lies in `[lo, hi]`. Concretely: every index `i < lo` has `a[i] < target`, and every index `i >= hi` has `a[i] >= target` (vacuously true when `hi == n`). Everything else follows mechanically. The two branches are not symmetric, and their asymmetry is the answer to the question. ## Why the passing branch keeps mid When `a[mid] >= target`, mid *satisfies the predicate*. But the algorithm is not looking for **a** satisfying index — it is looking for the **first** one. So mid is a candidate that may or may not be beaten by something to its left. Discarding it with `hi = mid - 1` destroys the invariant: after that assignment, index `mid` is neither below `lo` (so it is not known to fail) nor at-or-above `hi` (so it is not retained as known-good) — it has simply fallen out of the accounting, and if nothing to its left qualifies, the loop has no way to get back to it. A two-element counterexample settles it. Take `a = [3, 5]`, target `5`. Correct version: - `lo=0, hi=2` → `mid=1`, `a[1]=5 >= 5` → `hi=1`. - `lo=0, hi=1` → `mid=0`, `a[0]=3 < 5` → `lo=1`. - `lo == hi == 1` → return **1**. Correct: index 1 holds the first element at least 5. Broken version with `hi = mid - 1`: - `lo=0, hi=2` → `mid=1`, `a[1]=5 >= 5` → `hi=0`. - `lo == hi == 0`, loop exits → return **0**. Wrong: `a[0]` is 3, which is below the target. The bug does not crash, does not loop forever, and returns a plausible in-range index. That is what makes it dangerous — it is a wrong-answer bug, not a fault. ## Why the failing branch does use mid + 1 When `a[mid] < target`, mid is *definitively* not the answer, because it fails the predicate outright. Excluding it is not merely allowed, it is required for termination: if the failing branch wrote `lo = mid` instead, then with `hi == lo + 1` the midpoint rounds down to `lo`, the assignment changes nothing, and the loop spins forever. So the two branches have genuinely different obligations — one must retain its midpoint to stay correct, the other must exclude its midpoint to make progress. Candidates who try to "tidy" the loop into symmetry break one or the other. ## Why `while lo < hi` and not `<=` With a half-open range, `lo == hi` means *empty*: there is nothing left to examine, and by the invariant everything at or above `hi` qualifies while everything below `lo` does not, so `lo` is precisely the first qualifying index. Using `<=` would examine an index outside the intended range and, in the `hi == n` case, read past the end. Termination is easy to argue: `mid` always satisfies `lo <= mid < hi` when `lo < hi`, so `hi = mid` strictly decreases `hi` and `lo = mid + 1` strictly increases `lo`. The range shrinks by at least one every iteration, and roughly halves, giving O(log n) iterations. ## Why the midpoint is computed that way `mid = lo + (hi - lo) / 2` and `mid = (lo + hi) / 2` are mathematically identical and differ in fixed-width integer arithmetic: the second can overflow when both endpoints are large, producing a negative or wrapped midpoint and a wild memory access. The first form only ever adds a non-negative half-width to `lo`. This is a famous, long-undetected class of bug in published binary searches, and mainstream runtimes vary in whether the overflow silently wraps, traps, or is impossible because integers are arbitrary-precision — which is exactly why the safer form is worth writing unconditionally rather than reasoning about your host's arithmetic. ## The template generalises Nothing in the loop depends on the predicate being `a[i] >= target`. Swap in `a[i] > target` and the same code returns the upper bound. Any *monotone* predicate — false on a prefix, true on the remaining suffix — works unchanged, which is why this one loop is worth memorising as a shape rather than as a special case. What must be true is the monotonicity: if the predicate flips back and forth, halving discards the wrong side and the returned index is meaningless. ## Returning `lo` when nothing qualifies If the predicate is false everywhere, `lo` climbs all the way to `n` and the loop exits with `lo == hi == n`. Returning it is correct, not a fallthrough bug: `n` is the legal insertion position past the end. No separate "not found" branch is needed, and adding one is a sign the invariant was never understood.
- Why does the failing branch use mid + 1 rather than mid?Two reasons. Correctness allows it: `a[mid] < target` rules mid out outright, so nothing is lost. Termination requires it: when `hi == lo + 1` the midpoint rounds down to `lo`, so writing `lo = mid` would leave the range unchanged and spin forever. The branches are asymmetric because their obligations differ.
- Why is the loop condition lo < hi rather than lo <= hi?The range is half-open, so `lo == hi` already means empty — there is nothing left to test. With `<=` the loop would examine an index outside the intended range, and when `hi` equals the array length it would read past the end. The half-open convention is what makes the exit condition and the return value line up.
- Does this template still work if the predicate is not element comparison?Yes, provided the predicate is monotone over the index range: false on a prefix, true on the suffix. The loop only ever asks "does this index satisfy it?", so any such predicate returns the first true index. If the predicate flips back and forth, halving discards the wrong half and the result is meaningless rather than merely imprecise.
saying these in an interview costs you the question
- Copies hi = mid - 1 from the exact-match loop
- Claims the two branches must be symmetric
- Adds a not-found branch that returns a negative sentinel
- Uses lo <= hi with a half-open range
- Computes the midpoint as (lo + hi) / 2 without concern for overflow