// STUHUB · C++ DATA STRUCTURES
Binary Search: Invariants, Off-By-One Bugs, and the Exact Loop That Never Fails
Why binary search is easy to state and hard to write: the loop invariant, the three classic off-by-one patterns, and a version of the algorithm you can reproduce under exam pressure.
Introduction
Binary search finds a target in a sorted array in O(log n) by halving the search space each step. The idea takes one line to state; the implementation is famously bug-prone — Jon Bentley's classic exercise found that most professional programmers could not write it correctly. This note pins down the invariant that makes the loop provably correct, shows the three off-by-one patterns that break it, and ends with drills you can do before an exam.
The invariant that makes the loop correct
Every correct binary search maintains one invariant: if the target exists, it lies within [lo, hi] — the current, shrinking candidate range. The loop continues while the range is non-empty (lo <= hi); each iteration either finds the target or discards a half in a way that preserves the invariant; and when the range empties, the target is provably absent.
The subtle requirement is on the midpoint update. If arr[mid] < target, the target — if present — must be to the right of mid, so the new range starts at mid + 1. If arr[mid] > target, it must be to the left including nothing at mid, so the range ends at mid - 1. Getting +1/-1 wrong does not crash; it silently discards the answer or loops forever.
int binary_search(const int arr[], int n, int target) {
int lo = 0, hi = n - 1; // candidate range [lo, hi], inclusive
while (lo <= hi) { // range non-empty?
int mid = lo + (hi - lo) / 2; // overflow-safe midpoint
if (arr[mid] == target) return mid;
if (arr[mid] < target) lo = mid + 1; // discard [lo, mid]
else hi = mid - 1; // discard [mid, hi]
}
return -1; // range empty: not present
}The three classic bugs (and how each smells)
Bug 1 — missing +1/-1: lo = mid or hi = mid with the inclusive range. With a two-element range where mid rounds to lo, the range never shrinks and the loop spins forever. Smell: infinite loop on tiny inputs; test [1, 2] searching for 2.
Bug 2 — wrong loop condition: while (lo < hi) with lo <= hi-style updates. This exits before examining the last candidate. Smell: target at an endpoint is reported missing; test [5] searching for 5.
Bug 3 — overflow midpoint: (lo + hi) / 2 can overflow when both indices are large ints. Smell: works in tests, undefined behavior on huge arrays. The fix is lo + (hi - lo) / 2, or in C++20, std::midpoint(lo, hi).
The two variants exams actually ask for
Exact-match search (above) is the warm-up. The two variants that matter in exams and interviews:
Lower bound — the first index whose value is >= target. The invariant changes to "everything left of lo is < target":
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] < target) lo = mid + 1; // arr[mid] can't be the answer
else hi = mid; // arr[mid] might be the answer
}
return lo; // first index with arr[lo] >= targetNote the asymmetric update (hi = mid, no -1) matching the half-open convention — mixing the two conventions in one loop is bug pattern #1 in disguise. In production code, std::lower_bound implements exactly this.
Search in a rotated sorted array — decide which half is sorted by comparing endpoints, then check whether the target lies in the sorted half. Same loop skeleton, one extra branch.
// lower_bound: first index with arr[index] >= target
int lower_bound_idx(const int arr[], int n, int target) {
int lo = 0, hi = n; // half-open [lo, hi)
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo; // may be n: all values < target
}Why O(log n) — the counting argument
Each iteration halves the candidate range: n → n/2 → n/4 → ... → 1. The number of halvings until the range has one element is ⌈log₂ n⌉, so at most that many iterations, each O(1). For n = 1,000,000 that is ~20 comparisons; for n = 10⁹, ~30. This is also the exam favorite: "how many comparisons for n = 2²⁰?" — answer: at most 21 (20 halvings plus the final comparison).
The precondition is easy to state and easy to forget: the array must be sorted for the discard step to be valid. Running binary search on unsorted data returns garbage without any error — it is the algorithm's quietest failure mode.
Exercises with solutions
Work through each question before opening the solution below it.
Exercise 1
Trace binary_search on arr = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91] with target = 23. Write down lo, hi, mid at each step.
Solution
Step 1: lo=0, hi=9, mid=4 → arr[4]=16 < 23 → lo=5. Step 2: lo=5, hi=9, mid=7 → arr[7]=56 > 23 → hi=6. Step 3: lo=5, hi=6, mid=5 → arr[5]=23 == target → return 5. Three comparisons; the invariant held throughout (23 always inside [lo, hi]).
Exercise 2
A classmate's binary search infinite-loops on arr = [1, 3], target = 3. Their update is lo = mid. Diagnose and fix.
Solution
Trace: lo=0, hi=1, mid=0 → arr[0]=1 < 3 → lo = mid = 0. The range never changes: infinite loop. The bug: with lo <= hi (inclusive range), discarding the left half must skip mid, because arr[mid] was already examined and is not the target. Fix: lo = mid + 1. General rule: in an inclusive-range loop, both updates move strictly past mid.
Exercise 3
Write count_occurrences(arr, n, target) for a sorted array with duplicates, in O(log n), using lower/upper bounds.
Solution
Count = (index of first element > target) − (index of first element >= target).
int count_occ(const int arr[], int n, int target) {
int lo = 0, hi = n; // lower bound: first >= target
while (lo < hi) {
int m = lo + (hi - lo) / 2;
if (arr[m] < target) lo = m + 1; else hi = m;
}
int first_ge = lo;
hi = n; // upper bound: first > target
while (lo < hi) {
int m = lo + (hi - lo) / 2;
if (arr[m] <= target) lo = m + 1; else hi = m;
}
return lo - first_ge; // occurrences of target
}Both loops share the same midpoint idiom lo + (hi - lo) / 2 — writing them symmetrically is itself a bug guard. (In real code, prefer std::equal_range.)
Which C++ standard does this page assume?
Standard C++17. C++ began as C with Classes in 1979 and first shipped commercially in 1985; what a compiler flag actually selects today is one of the ISO revisions below, and the committee has published a new one every 3 years since 2011. Build these listings with -std=c++17 and they compile as written.
| Revision | Published as | What it changes for the code on this page |
|---|---|---|
| C++98 | ISO/IEC 14882:1998 | The first ISO C++, and the dialect most data-structures courses still teach from: templates, the STL containers, and raw pointers doing the work. |
| C++03 | ISO/IEC 14882:2003 | A defect-fix revision. Nothing on these pages depends on it, and nothing on these pages is broken by it. |
| C++11 | ISO/IEC 14882:2011 | Where nullptr, auto, range-based for, move semantics and the unordered containers arrive. Every listing here writes nullptr rather than NULL because of it. |
| C++14 | ISO/IEC 14882:2014 | A small revision: generic lambdas and std::make_unique. Used only where it makes ownership clearer. |
| C++17 | ISO/IEC 14882:2017 | What every listing on StuHub targets and was compiled against. If you build these files, build them with -std=c++17. |
| C++20 | ISO/IEC 14882:2020 | Concepts, ranges and std::midpoint. Flagged in the prose where it offers a shorter correct form, never assumed by the code. |
| C++23 | ISO/IEC 14882:2024 | Not used here. Named so you can tell whether a snippet you found elsewhere will compile on a lab machine that predates it. |
Common questions
What does this page cover?
Why binary search is easy to state and hard to write: the loop invariant, the three classic off-by-one patterns, and a version of the algorithm you can reproduce under exam pressure.
How long does this page take to work through?
About 5 minutes of reading at 200 words per minute, plus 3 questions with worked solutions at the foot of the page. Reading it end to end is the slow way; the intended use is to find the section you are stuck on, then do the questions for that section with the solutions covered.
Which C++ standard do these examples target?
Standard C++17 — ISO/IEC 14882:2017. Every listing was compiled with -std=c++17 and -Wall -Wextra before publication, and the linked-structure examples were also run under AddressSanitizer. Where C++20 offers a shorter correct form, such as std::midpoint, the prose says so instead of quietly using it.
Is StuHub free, and do I need an account?
It is free and there is nothing to sign in to. No login, account or payment is required to read any of the 20 topics — StuHub is published by DUOCODE TECHNOLOGY alongside APRide, and the ride board's accounts have nothing to do with it.
Can I paste this code into my assignment?
Treat it as a reference, not as an answer key. StuHub is educational material only, it is not coursework and it is not affiliated with or endorsed by any institution, so your own submission rules decide what you may reuse. Every listing was compiled and run before publication, and you should still compile and test anything you take.
Why is everything written in C++ rather than pseudocode?
Because most of the mistakes worth catching are C++ mistakes, not algorithm mistakes: a lost pointer, a destructor that never runs, an index that underflows because it was unsigned. Pseudocode hides exactly the layer where a data-structures assignment is actually failed.
Where should I check what the standard library really guarantees?
cppreference for the day-to-day answer, and the WG21 working drafts when the exact wording matters — both are linked below. Compiler documentation settles the rest: a warning you cannot explain is usually the compiler being right.
Does StuHub replace my lecture notes?
No. It is written to sit beside them: your course decides what is examinable, in what notation, and with which library restrictions. Where this page and your module handbook disagree about scope, the handbook wins.
Is StuHub connected to Asia Pacific University?
No. It is an independent reference published by DUOCODE TECHNOLOGY, not affiliated with or endorsed by Asia Pacific University or any other institution. It was written for APU students because that is who asked for it, and it is open to anyone.
Where can I check this against the language itself?
Nothing on this page outranks the standard or the library reference. When this page and one of these disagree, they are right and we want to know.
- cppreference — the C++ standard library referenceThe fastest correct answer for what a container or algorithm actually guarantees.
- isocpp.org — the Standard C++ FoundationThe FAQ and the core guidelines, written by the people who define the language.
- ISO/IEC JTC1/SC22/WG21 — the C++ standards committeeWorking drafts of the standard itself, free to read, when the wording is the question.
- GCC online documentationWhat -Wall, -Wextra and the sanitizers used on these listings actually check.
- Microsoft Learn — C++ language documentationThe MSVC view, for readers whose lab machines build with Visual Studio.
Published 2026-08-09 · updated 2026-08-27 · DUOCODE TECHNOLOGY