Conceptual Foundation & Intuitive Analogy
🏟️ The Analogy: The High-Low Number Guessing Game
Imagine guessing a secret number between 1 and 100. If your first guess is 50, and the host says "Too High!", you don't just know that 50 is wrong—you know that 51 through 100 are completely impossible. One question rules out 50 candidates. Your next guess is 25 (halfway between 1 and 49). If the host says "Too Low!", numbers 1 through 25 are ruled out. Within at most questions, you are guaranteed to pin down the secret number out of 100 candidates. Binary search executes this exact elimination algorithm on computer memory.
🔑 Why It Works
The invariant that guarantees correctness is interval enclosure: at the start of every while-loop iteration, if target exists anywhere in nums, it is guaranteed to reside in nums[lo..hi]. When nums[mid] < target, the subarray nums[lo..mid] is strictly smaller than target, so advancing lo = mid + 1 preserves the enclosure without dropping the target.
At every moment, if an answer exists at all, it lies inside [left, right]. Every branch We take preserves that.
Both L and R are inclusive valid indices. Candidate space shrinks symmetrically via lo = mid + 1 or hi = mid - 1. When lo == hi, exactly one candidate element remains to be inspected.
R is an exclusive upper bound. Search space shrinks asymmetrically: hi = mid preserves the first valid candidate, while lo = mid + 1 discards the false prefix. Converges deterministically on lo == hi without post-loop adjustments.
bisect_left (LC 35), Answer Space Bisection (LC 1011, LC 875).The 2 Mental Models of Binary Search
Binary search applies whenever candidate feasibility is monotonic. Name the model before coding.
1Lower Bound
candidate domain ➔ monotonic boolean sequence [F, F, ..., T, T] ➔ first TrueLower Bound (First True Binary Search)
| 1 | from typing import Callable, List |
| 2 | |
| 3 | |
| 4 | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 5 | """First candidate in [lo, hi) where is_feasible is True. |
| 6 | Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 7 | Returns hi when no candidate is True. N = hi - lo candidates. T = the cost of one is_feasible call. |
| 8 | Time: O(T * log N) | Space: O(1) |
| 9 | """ |
| 10 | while lo < hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | |
| 13 | # 1. Check the Midpoint: |
| 14 | # is_feasible(mid) shows which side of the flip mid is on: before the answer (False), |
| 15 | # or at the answer or after it (True). |
| 16 | if is_feasible(mid): |
| 17 | # 2. Keep the Candidate: |
| 18 | # mid may be the first True, so keep it in the range by moving hi down to mid. |
| 19 | hi = mid |
| 20 | else: |
| 21 | # 3. Discard the Candidate: |
| 22 | # mid is False, so the first True must be strictly to the right of mid. |
| 23 | lo = mid + 1 |
| 24 | |
| 25 | # 4. Return the Boundary: |
| 26 | # The loop stops when lo equals hi; that position is the first True, or hi when none was True. |
| 27 | return lo |
is_feasible is False before the answer and True from the answer on, so one call at mid decides the move, which is hi = mid after a True and lo = mid + 1 after a False. A True mid may be the first True, so hi = mid keeps it in the range. A False mid is not the answer and the first True is to its right, so lo = mid + 1 drops it. After either move the first True is still in [lo, hi]. hi is allowed to be the answer, because it starts one past the last candidate, so a row with no True returns hi. The range gets smaller on every pass and lo never passes hi, so the loop stops with lo == hi, and lo is the answer. The trap is lo = mid in the False branch. The midpoint is rounded down, so when hi == lo + 1, mid equals lo. Then lo does not move, and the loop never ends.
Gotcha 1 of 7
- The trap
Gotcha 1: Writing
lo = midinstead oflo = mid + 1Mistakelo = midFixlo = mid + 1What happens: When one candidate is left (
hiis equal tolo + 1),midequalslo. If the check is False there,lo = midleaveslowhere it was. The range never gets smaller, so the loop never ends.Example: Search Insert Position (LC 35): nums = [1, 3, 5, 6], target = 5Mistake: the loop never endsFix: returns 2
2Upper Bound
candidate domain ➔ monotonic boolean sequence [T, T, ..., F, F] ➔ last TrueUpper Bound (Last True Binary Search)
| 1 | from typing import Callable |
| 2 | |
| 3 | |
| 4 | def binary_search_upper_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 5 | """Last candidate in [lo, hi) where is_feasible is True; lo - 1 when none is True.""" |
| 6 | # lo becomes a known True, or the sentinel one before the first candidate; hi becomes the last candidate. |
| 7 | lo, hi = lo - 1, hi - 1 |
| 8 | |
| 9 | while lo < hi: |
| 10 | # 1. Compute the Ceiling Midpoint: |
| 11 | # Round mid up so lo < mid <= hi; rounding down would leave mid stuck at lo when one candidate remains. |
| 12 | mid = lo + (hi - lo + 1) // 2 |
| 13 | |
| 14 | # 2. Check the Midpoint: |
| 15 | # is_feasible(mid) shows which side of the flip mid is on: True at mid or before it, or False after it. |
| 16 | if is_feasible(mid): |
| 17 | # 3. Keep the Candidate: |
| 18 | # mid may be the last True, so keep it in the range by moving lo up to mid. |
| 19 | lo = mid |
| 20 | else: |
| 21 | # 4. Discard the Candidate: |
| 22 | # mid is False, so the last True must be strictly to the left of mid. |
| 23 | hi = mid - 1 |
| 24 | |
| 25 | # 5. Return the Boundary: |
| 26 | # The loop stops when lo equals hi; that position is the last True, or the sentinel when none was True. |
| 27 | return lo |
The check reads True…True False…False and the answer is the last True, so a True mid must stay in the range with lo = mid, and a False mid is dropped with hi = mid - 1. lo = mid can leave lo where it was, so the midpoint rounds up with mid = lo + (hi - lo + 1) // 2, which gives lo < mid <= hi. Then lo = mid always moves lo forward. The template first shifts both bounds down by 1 with lo, hi = lo - 1, hi - 1. After the shift, lo starts at the sentinel, which is the position lo - 1 just before the first candidate, and later holds a candidate that passed the check. hi starts at the last candidate. The sentinel is the value that the call returns when no candidate is True. The last True stays in [lo, hi] after both moves, and each move makes the range smaller, so the loop stops with lo == hi and returns lo. The trap is the rounded-down midpoint lo + (hi - lo) // 2 together with lo = mid. When hi == lo + 1, mid equals lo, a True check leaves lo where it was, and the loop never ends.
Gotcha 1 of 7
- The trap
Gotcha 1: Using the round-down midpoint with
lo = midMistakemid = lo + (hi - lo) // 2Fixmid = lo + (hi - lo + 1) // 2What happens: When two possible answers are left (
loandhi, withhi == lo + 1),lo + (hi - lo) // 2is equal tolo. If the check is True there,lo = midleaveslowhere it was. The range does not become smaller, and the loop never ends. The round-up midpoint is always larger thanlo.Example: Sqrt(x) (LC 69): x = 8Mistake: the loop never endsFix: returns 2
Built on Lower Bound
- Exact MatchClassic Binary Search
It asks Lower Bound for the first value that is at least
target, and then checks whether that value is equal totarget. - Extrema SearchPeak Finding (Bitonic Binary Search)
The check compares a value with the next value, so the search works on values that are not sorted, when they rise and then fall.
- Rotated ArraySearch in Rotated Sorted Array
The check compares a value with the first value of the list, and it runs twice: once to find the pivot, and once to search the sorted order that starts at the pivot.
- Feasible FunctionBinary Search on Answer (Parametric Search)
The candidates are possible answers, not positions in a list, and the check simulates the problem for one answer.
- Kth SmallestBinary Search on Value Range (Kth Smallest in Sorted Matrix)
The check counts the values that are at most the candidate, and the answer is the smallest candidate whose count is at least
k. - Median of Two Sorted ArraysPartition Binary Search (Median of Two Sorted Arrays)
The candidate is how many values to take from the shorter list, not a value, and the check compares the values on both sides of the cut.
- Continuous BisectionBinary Search on Real Numbers (Bisection Method)
The candidates are real numbers, so the loop runs a fixed number of times instead of waiting until
lois equal tohi. - Global Matrix BisectionFlattened Index Binary Search (Search a 2D Matrix)
A row-major index maps to a matrix cell, and one guarded look confirms equality after Lower Bound.
The Binary Search Decision Rule
Which card do I need? Ask these questions in this order:
Use the Lower Bound loop with a check that is False and then True.
Use Lower Bound to find the first value that is at least the target, then check whether that value is equal to the target.
Use Upper Bound with the ceiling midpoint; keep a passing mid and discard a failing mid.
Find the pivot, the first value that is smaller than the first value of the list, and then search the sorted order that starts at the pivot.
Compare each value with the next value. On a mountain the check is False then True; with several peaks, each update preserves some peak.
Search the range of possible answers, and use the simulation as the check.
Search the range of values, and count how many values are at most the candidate.
Search the place to cut the shorter list, and compare the values on both sides of the two cuts.
Use Lower Bound on flat row-major indices and one guarded equality check.
Run the loop a fixed number of times on real numbers, instead of waiting until `lo` is equal to `hi`.
Pattern Architecture & Invariant Design Masterclass
Binary search is not "an algorithm for finding a number in a sorted array." It is a decision procedure: you have a range of candidate answers, you can ask one yes/no question about the middle candidate, and the answer to that question is guaranteed to tell you which half of the range to throw away. Because you throw away half every time, a range of one million candidates is exhausted in about twenty questions instead of one million.
The one thing that must be true for this to work is monotonicity: as you move from left to right across the candidate range, the answer to your yes/no question flips from No to Yes exactly once and never flips back — [No, No, No, Yes, Yes, Yes]. If the answers can flip back and forth, discarding a half is unsound and the whole method collapses. Sortedness is just the most common way monotonicity shows up; it is not the requirement itself.
What an invariant is (this term recurs in every pattern, so it is worth one definition): an invariant is a statement that is true before the loop starts, still true after every single pass, and therefore true when the loop ends. It is how you prove code is right without running it — and in an interview, stating your invariant out loud is worth more than the code itself, because it shows you know why the algorithm terminates with the right answer rather than that you remember its shape.
Invariant-First Boundary Design (and the "Pair vs. Target" Heuristic)
Boundary conditions (< vs <=) and pointer shrinkage rules (mid ± 1 vs mid) are not arbitrary choices or keyword rules to memorize. In technical interviews, senior interviewers specifically test whether you can derive loop conditions directly from your candidate search space invariant:
1. Closed Candidate Space (while left <= right:)
- The Invariant: At the start of every iteration, if a solution exists, it is guaranteed to lie in the closed index range
[left, right]. - The Halting Condition: The candidate set (closed candidate interval
[left, right]) is non-empty wheneverleft <= right. Whenleft == right, exactly one candidate element remains to be inspected (the singleton state). - When to Use: Standard Exact-Match Search (LC 704) where an immediate equality match triggers an early return, or where you shrink until
left > rightand return a sentinel if not found.
2. Half-Open Candidate Space (while left < right:)
- The Invariant: The first True is in
[left, right]: every index belowleftis False, and every index fromrighton is True or past the end. "Half-open" describes the indices still unchecked,[left, right):rightitself is already known to be True (or past the end), so it stays in the answer range as the fallback, never excluded from it. - The Halting Condition: The loop runs while
left < rightbecause one candidate left (left == right) is already the answer, not becauserightis excluded. At termination,left(andright) points precisely to the boundary index without requiring post-loop fixups. - When to Use: Lower Bound / First Feasible Search (
bisect_left, LC 35, LC 1011 Capacity to Ship Packages, LC 875 Koko Eating Bananas). - Cheat Sheet: positions vs. indices, why
right = len(nums), and the bounds check before readingnums[left]are summarized in Algorithm Cheat Sheet → Binary Search Bounds.
3. Deconstructing "Target": Why the Term Can Mislead
"Target" is an overloaded term in algorithmic problems. Depending on what you are looking for, the invariant changes:
- Exact-Match Target: Looking for a specific key value in an array typically uses closed interval with
left <= rightand 3-way branching (==,<,>). - Insertion Point / Lower Bound Target: Finding the first index where
nums[i] >= targettypically uses half-open withleft < rightandright = mid. - Boundary / Feasibility Target (Answer Space): Finding the minimum valid configuration (capacity, speed, days) where
is_valid(mid)holds uses monotonic predicate boundary searchleft < rightwithright = mid.
4. The "Pair vs. Target" Heuristic (Rule of Thumb)
The popular "Pair vs. Target" trick is a memorable starting heuristic, but it is not a universal law for pointer or binary-search loops:
- The Heuristic: Developers often reach for
<when needing a pair of distinct elements (Two Sum II, Valid Palindrome) becauseleft == rightis degenerate, and<=when testing a target in a closed candidate space so the final singleton is examined. - Why It Is Not Universal: Correct boundary conditions depend on the precise invariant, whether endpoints are inclusive or exclusive , whether a problem needs the first/last valid position or an exact match, and the update rules for
left,right, andmid. For instance, lower-bound and feasibility searches evaluate targets withwhile left < rightandright = midwithout post-processing, while 3-way Dutch National Flag partitioning inspects elements withwhile mid <= high. - Senior Formulation: State your candidate invariant first: "I am maintaining the candidate answer in
[left, right]. Since the candidate set is non-empty wheneverleft <= right, my loop condition iswhile left <= right:, and my updates aremid + 1andmid - 1."
Common Questions
"Binary search is for finding a value in a sorted array. So how does it apply to something like minimizing the maximum ship capacity (LC 1011), where there's no sorted array of capacities to search at all?"
Because sortedness was never the actual requirement — it's just the most familiar disguise of the real one, monotonicity.
Standard search asks "is
nums[mid]too small, too big, or exactly the target?" — three outcomes, and the array's sortedness is what guarantees discarding a half is safe. But binary search only ever needs one yes/no question whose answer flips exactly once as you scan the candidate range:[No, No, No, Yes, Yes, Yes]. A sorted array happens to produce that pattern for "isnums[mid] >= target?" — but so does an entirely invented, unsorted range of candidate answers, as long as the yes/no question you ask about them is monotonic.For "ship packages within
Ddays," the array of package weights is never binary-searched — it's a fixed input, sorted or not. What gets binary-searched is an invented range of possible ship capacities, frommax(weights)tosum(weights). The questioncan_ship(mid)("can a ship of capacitymidfinish inDdays or fewer?") is monotonic: every capacity below the true minimum answersNo, every capacity at or above it answersYes. That single monotonic predicate is the whole requirement — which is why the same algorithm also solves Koko Eating Bananas and Split Array Largest Sum, none of which involve searching a sorted array of anything.
Coach discussion points
- 1.
"What exactly makes this array searchable in
O(log n)? Give me the predicate and prove it never flips back." — Tests whether the student has internalised monotonicity or has merely memorised "sorted array → binary search". - 2.
"You wrote
while lo <= hiandhi = mid. Walk me throughlo == hiand tell me what happens." — Forces the student to discover the infinite loop by tracing rather than being told. - 3.
"In the capacity problem you said
O(log n). What isn, and what does one iteration actually cost?" — Probes whether the predicate's ownO(n)cost was multiplied in. This is the single highest-signal follow-up in this pattern. - 4.
"You have found a match on the first probe and you are about to return. Under what version of this question would returning now be a bug?" — Surfaces the duplicates/boundary distinction, and the difference between "found one" and "found the first".
- 5.
"In the median-of-two-sorted-arrays solution, why do you always binary search the SHORTER array? What changes if you search the longer one instead?" — Tests whether the student understands
j = half - iis a forced arithmetic consequence, not an independent choice, and that searching the shorter array only lowers the cost, fromO(log max(m, n))toO(log min(m, n)). The range[max(0, k - n), min(m, k) + 1)keeps every read inside both lists for either order. - 6.
"Walk me through why
x - arr[mid] > arr[mid + k] - xis monotonic across window left-edges, when neither side is compared to a fixed target." — Probes whether the student can identify monotonicity in a relative, window-shift comparison rather than only in the more familiar "compare to a fixed value" shape. - 7.
"In Search a 2D Matrix you wrote
divmod(mid, n). Which number isn, and what woulddivmod(mid, m)read on a 3 x 4 grid?" — Makes the student find the row-count bug by tracing one flat index, instead of being told that a square test hides it. - 8.
"Search a 2D Matrix II (LC 240) also has sorted rows. Why does the same flat-index search give wrong answers there?" — Tests whether the student knows the flat index needs each row to start after the previous one ends, not just sorted rows.
- 9.
"In Sqrt(x) you wrote
lo = midwhen the check passes. Tracex = 4fromlo = 2, hi = 3withmid = lo + (hi - lo) // 2. What happens, and what one-character change fixes it?" — Makes the student find the floor-midpoint infinite loop of Upper Bound (last True) by tracing, and see why the midpoint must round up once a passingmidis kept withlo = mid. - 10.
"C++ calls its 'first value greater than target' search
upper_bound. Is that a first True or a last True, and how would you get the last index whose value is at mosttarget?" — Separates the library name from the model:upper_bound/bisect_rightis a Lower Bound (first True) search; the last index<= targetis that minus one, or an Upper Bound search. - 11.
"In Separate Squares I,
loandhiare two floats with nothing between them. What does(lo + hi) / 2give, and what doeswhile lo < hido from there?" — Makes the student find the float infinite loop of Continuous Bisection by reasoning about one round, and see why the loop must count rounds. - 12.
"In K-th Smallest Prime Fraction you have
hiafter 100 rounds, but the answer must be a pair. Which fraction do you return, and why can no other fraction be between it andhi?" — Tests the snap: the final window is narrower than the smallest gap between two fractions, about1 / (3 * 104)^2. - 13.
"In Find Minimum in Rotated Sorted Array II,
nums[mid] == nums[hi]. Ismidin the left run or the right one? Show me an array for each answer." — Makes the student build[1, 1, 1, 0, 1]and[1, 0, 1, 1, 1]and see that a tie picks no side, so it can only drophi. - 14.
"Why is dropping
hion a tie safe, and what does that step cost when every value is equal? Can any algorithm do better?" — Tests the invariant (a copy ofnums[hi]stays atmid) and the lower bound: with1s and one hidden0, every unread index could hold the0.
Complexity analysis
Time: O(log n).
Each iteration replaces the live interval with one of its halves, so after k iterations at most n / 2^k candidates remain. The loop ends when that reaches 1, i.e. when k = log₂ n. Formally T(n) = T(n/2) + O(1), which by the Master Theorem (case 2, a = 1, b = 2, f(n) = O(1)) gives Θ(log n).
Concretely: 1,000,000 candidates need at most 20 questions. 1,000,000,000 need at most 30.
Where that changes: in archetype 4.4 the predicate is not O(1) — it is an O(n) greedy scan. Total cost is therefore O(n · log(sum - max)), not O(log n). Students routinely quote O(log n) here and are wrong; the interviewer is checking whether you multiplied by the predicate's own cost.
Space: O(1).
The iterative form holds exactly three integers (lo, hi, mid) plus one accumulator. No allocation, no recursion. The recursive form is O(log n) because each halving pushes a stack frame — a real, if small, difference worth naming aloud.
You are looking up the word "monotone" in a 1,000-page paper dictionary. You do not start at page 1 and turn pages one at a time. You open the book near the middle, land on page 500, read the first word on it, and see it starts with "L". "M" comes after "L", so you have just eliminated pages 1-500 without reading a single one of them. You then repeat on pages 501-1000.
The same shape runs inside real systems every day:
- A PostgreSQL B-Tree index descends toward a row id by comparing keys and discarding subtrees.
- A deployment tool bisecting a broken build (git bisect) halves the commit range on each "is it broken here? yes/no".
- A capacity planner asking "can 2 machines finish this workload in <= T minutes?" and shrinking T.
💡 Don't worry if boundary decisions feel subtle right now! Binary search is one fundamental bisection principle applied across monotonic domains. As you progress through the 9 Archetype Variants (from Exact Match and Rotated Minimum to Capacity Allocation and Median of Two Sorted Arrays), each hands-on challenge will cement this intuition until choosing between lo <= hi vs lo < hi and writing hi = mid - 1 vs hi = mid becomes second nature.
Integer overflow on midpoint calculation: In 32-bit environments (Java/C++/C#),
(left + right) // 2overflows whenleft + right > 231 - 1. Always calculate midpoint asleft + (right - left) // 2.Infinite loop via unreduced interval (
left = mid): Whenright - left = 1, integer division floorsmidtoleft. Assigningleft = midfails to shrink the search space, looping forever. Useleft = mid + 1or half-open intervals.Applying binary search to unsorted inputs: Binary search fundamentally requires a monotonic predicate. Applying it to an unsorted or partially sorted list produces arbitrary incorrect results with no warning.
Staff+ Systems Architecture & Hardware Considerations
Triggers: "sorted order", "monotonic predicate f(x) -> bool", "minimize the maximum / maximize the minimum", "O(log N) runtime budget". Binary search is fundamentally not about looking up numbers in arrays; it is the algorithmic exploitation of monotonicity to eliminate half the remaining candidate solution space in decision steps.
Discrete search space size up to (answer space bisection), or array index boundaries up to . A logarithmic time budget allows traversing an astronomical element space in at most 60 predicate evaluations. Space budget is strictly registers.
Three classic pitfalls: (1) Integer overflow when computing midpoint in 32-bit systems (mitigate via ); (2) Off-by-one infinite loops caused by mismatched loop conditions (while lo < hi vs while lo <= hi) with incorrect boundary assignments; (3) Failure to establish a strictly monotonic boolean predicate.
Conceptual Foundation & Intuitive Analogy
🏟️ The Analogy: The High-Low Number Guessing Game
Imagine guessing a secret number between 1 and 100. If your first guess is 50, and the host says "Too High!", you don't just know that 50 is wrong—you know that 51 through 100 are completely impossible. One question rules out 50 candidates. Your next guess is 25 (halfway between 1 and 49). If the host says "Too Low!", numbers 1 through 25 are ruled out. Within at most questions, you are guaranteed to pin down the secret number out of 100 candidates. Binary search executes this exact elimination algorithm on computer memory.
🔑 Why It Works
The invariant that guarantees correctness is interval enclosure: at the start of every while-loop iteration, if target exists anywhere in nums, it is guaranteed to reside in nums[lo..hi]. When nums[mid] < target, the subarray nums[lo..mid] is strictly smaller than target, so advancing lo = mid + 1 preserves the enclosure without dropping the target.
At every moment, if an answer exists at all, it lies inside [left, right]. Every branch We take preserves that.
Both L and R are inclusive valid indices. Candidate space shrinks symmetrically via lo = mid + 1 or hi = mid - 1. When lo == hi, exactly one candidate element remains to be inspected.
R is an exclusive upper bound. Search space shrinks asymmetrically: hi = mid preserves the first valid candidate, while lo = mid + 1 discards the false prefix. Converges deterministically on lo == hi without post-loop adjustments.
bisect_left (LC 35), Answer Space Bisection (LC 1011, LC 875).The 2 Mental Models of Binary Search
Binary search applies whenever candidate feasibility is monotonic. Name the model before coding.
1Lower Bound
candidate domain ➔ monotonic boolean sequence [F, F, ..., T, T] ➔ first TrueLower Bound (First True Binary Search)
| 1 | from typing import Callable, List |
| 2 | |
| 3 | |
| 4 | def binary_search_lower_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 5 | """First candidate in [lo, hi) where is_feasible is True. |
| 6 | Precondition: is_feasible is monotonic (False, False, ..., True, True). |
| 7 | Returns hi when no candidate is True. N = hi - lo candidates. T = the cost of one is_feasible call. |
| 8 | Time: O(T * log N) | Space: O(1) |
| 9 | """ |
| 10 | while lo < hi: |
| 11 | mid = lo + (hi - lo) // 2 |
| 12 | |
| 13 | # 1. Check the Midpoint: |
| 14 | # is_feasible(mid) shows which side of the flip mid is on: before the answer (False), |
| 15 | # or at the answer or after it (True). |
| 16 | if is_feasible(mid): |
| 17 | # 2. Keep the Candidate: |
| 18 | # mid may be the first True, so keep it in the range by moving hi down to mid. |
| 19 | hi = mid |
| 20 | else: |
| 21 | # 3. Discard the Candidate: |
| 22 | # mid is False, so the first True must be strictly to the right of mid. |
| 23 | lo = mid + 1 |
| 24 | |
| 25 | # 4. Return the Boundary: |
| 26 | # The loop stops when lo equals hi; that position is the first True, or hi when none was True. |
| 27 | return lo |
is_feasible is False before the answer and True from the answer on, so one call at mid decides the move, which is hi = mid after a True and lo = mid + 1 after a False. A True mid may be the first True, so hi = mid keeps it in the range. A False mid is not the answer and the first True is to its right, so lo = mid + 1 drops it. After either move the first True is still in [lo, hi]. hi is allowed to be the answer, because it starts one past the last candidate, so a row with no True returns hi. The range gets smaller on every pass and lo never passes hi, so the loop stops with lo == hi, and lo is the answer. The trap is lo = mid in the False branch. The midpoint is rounded down, so when hi == lo + 1, mid equals lo. Then lo does not move, and the loop never ends.
Gotcha 1 of 7
- The trap
Gotcha 1: Writing
lo = midinstead oflo = mid + 1Mistakelo = midFixlo = mid + 1What happens: When one candidate is left (
hiis equal tolo + 1),midequalslo. If the check is False there,lo = midleaveslowhere it was. The range never gets smaller, so the loop never ends.Example: Search Insert Position (LC 35): nums = [1, 3, 5, 6], target = 5Mistake: the loop never endsFix: returns 2
2Upper Bound
candidate domain ➔ monotonic boolean sequence [T, T, ..., F, F] ➔ last TrueUpper Bound (Last True Binary Search)
| 1 | from typing import Callable |
| 2 | |
| 3 | |
| 4 | def binary_search_upper_bound(lo: int, hi: int, is_feasible: Callable[[int], bool]) -> int: |
| 5 | """Last candidate in [lo, hi) where is_feasible is True; lo - 1 when none is True.""" |
| 6 | # lo becomes a known True, or the sentinel one before the first candidate; hi becomes the last candidate. |
| 7 | lo, hi = lo - 1, hi - 1 |
| 8 | |
| 9 | while lo < hi: |
| 10 | # 1. Compute the Ceiling Midpoint: |
| 11 | # Round mid up so lo < mid <= hi; rounding down would leave mid stuck at lo when one candidate remains. |
| 12 | mid = lo + (hi - lo + 1) // 2 |
| 13 | |
| 14 | # 2. Check the Midpoint: |
| 15 | # is_feasible(mid) shows which side of the flip mid is on: True at mid or before it, or False after it. |
| 16 | if is_feasible(mid): |
| 17 | # 3. Keep the Candidate: |
| 18 | # mid may be the last True, so keep it in the range by moving lo up to mid. |
| 19 | lo = mid |
| 20 | else: |
| 21 | # 4. Discard the Candidate: |
| 22 | # mid is False, so the last True must be strictly to the left of mid. |
| 23 | hi = mid - 1 |
| 24 | |
| 25 | # 5. Return the Boundary: |
| 26 | # The loop stops when lo equals hi; that position is the last True, or the sentinel when none was True. |
| 27 | return lo |
The check reads True…True False…False and the answer is the last True, so a True mid must stay in the range with lo = mid, and a False mid is dropped with hi = mid - 1. lo = mid can leave lo where it was, so the midpoint rounds up with mid = lo + (hi - lo + 1) // 2, which gives lo < mid <= hi. Then lo = mid always moves lo forward. The template first shifts both bounds down by 1 with lo, hi = lo - 1, hi - 1. After the shift, lo starts at the sentinel, which is the position lo - 1 just before the first candidate, and later holds a candidate that passed the check. hi starts at the last candidate. The sentinel is the value that the call returns when no candidate is True. The last True stays in [lo, hi] after both moves, and each move makes the range smaller, so the loop stops with lo == hi and returns lo. The trap is the rounded-down midpoint lo + (hi - lo) // 2 together with lo = mid. When hi == lo + 1, mid equals lo, a True check leaves lo where it was, and the loop never ends.
Gotcha 1 of 7
- The trap
Gotcha 1: Using the round-down midpoint with
lo = midMistakemid = lo + (hi - lo) // 2Fixmid = lo + (hi - lo + 1) // 2What happens: When two possible answers are left (
loandhi, withhi == lo + 1),lo + (hi - lo) // 2is equal tolo. If the check is True there,lo = midleaveslowhere it was. The range does not become smaller, and the loop never ends. The round-up midpoint is always larger thanlo.Example: Sqrt(x) (LC 69): x = 8Mistake: the loop never endsFix: returns 2
Built on Lower Bound
- Exact MatchClassic Binary Search
It asks Lower Bound for the first value that is at least
target, and then checks whether that value is equal totarget. - Extrema SearchPeak Finding (Bitonic Binary Search)
The check compares a value with the next value, so the search works on values that are not sorted, when they rise and then fall.
- Rotated ArraySearch in Rotated Sorted Array
The check compares a value with the first value of the list, and it runs twice: once to find the pivot, and once to search the sorted order that starts at the pivot.
- Feasible FunctionBinary Search on Answer (Parametric Search)
The candidates are possible answers, not positions in a list, and the check simulates the problem for one answer.
- Kth SmallestBinary Search on Value Range (Kth Smallest in Sorted Matrix)
The check counts the values that are at most the candidate, and the answer is the smallest candidate whose count is at least
k. - Median of Two Sorted ArraysPartition Binary Search (Median of Two Sorted Arrays)
The candidate is how many values to take from the shorter list, not a value, and the check compares the values on both sides of the cut.
- Continuous BisectionBinary Search on Real Numbers (Bisection Method)
The candidates are real numbers, so the loop runs a fixed number of times instead of waiting until
lois equal tohi. - Global Matrix BisectionFlattened Index Binary Search (Search a 2D Matrix)
A row-major index maps to a matrix cell, and one guarded look confirms equality after Lower Bound.
The Binary Search Decision Rule
Which card do I need? Ask these questions in this order:
Use the Lower Bound loop with a check that is False and then True.
Use Lower Bound to find the first value that is at least the target, then check whether that value is equal to the target.
Use Upper Bound with the ceiling midpoint; keep a passing mid and discard a failing mid.
Find the pivot, the first value that is smaller than the first value of the list, and then search the sorted order that starts at the pivot.
Compare each value with the next value. On a mountain the check is False then True; with several peaks, each update preserves some peak.
Search the range of possible answers, and use the simulation as the check.
Search the range of values, and count how many values are at most the candidate.
Search the place to cut the shorter list, and compare the values on both sides of the two cuts.
Use Lower Bound on flat row-major indices and one guarded equality check.
Run the loop a fixed number of times on real numbers, instead of waiting until `lo` is equal to `hi`.
Pattern Architecture & Invariant Design Masterclass
Binary search is not "an algorithm for finding a number in a sorted array." It is a decision procedure: you have a range of candidate answers, you can ask one yes/no question about the middle candidate, and the answer to that question is guaranteed to tell you which half of the range to throw away. Because you throw away half every time, a range of one million candidates is exhausted in about twenty questions instead of one million.
The one thing that must be true for this to work is monotonicity: as you move from left to right across the candidate range, the answer to your yes/no question flips from No to Yes exactly once and never flips back — [No, No, No, Yes, Yes, Yes]. If the answers can flip back and forth, discarding a half is unsound and the whole method collapses. Sortedness is just the most common way monotonicity shows up; it is not the requirement itself.
What an invariant is (this term recurs in every pattern, so it is worth one definition): an invariant is a statement that is true before the loop starts, still true after every single pass, and therefore true when the loop ends. It is how you prove code is right without running it — and in an interview, stating your invariant out loud is worth more than the code itself, because it shows you know why the algorithm terminates with the right answer rather than that you remember its shape.
Invariant-First Boundary Design (and the "Pair vs. Target" Heuristic)
Boundary conditions (< vs <=) and pointer shrinkage rules (mid ± 1 vs mid) are not arbitrary choices or keyword rules to memorize. In technical interviews, senior interviewers specifically test whether you can derive loop conditions directly from your candidate search space invariant:
1. Closed Candidate Space (while left <= right:)
- The Invariant: At the start of every iteration, if a solution exists, it is guaranteed to lie in the closed index range
[left, right]. - The Halting Condition: The candidate set (closed candidate interval
[left, right]) is non-empty wheneverleft <= right. Whenleft == right, exactly one candidate element remains to be inspected (the singleton state). - When to Use: Standard Exact-Match Search (LC 704) where an immediate equality match triggers an early return, or where you shrink until
left > rightand return a sentinel if not found.
2. Half-Open Candidate Space (while left < right:)
- The Invariant: The first True is in
[left, right]: every index belowleftis False, and every index fromrighton is True or past the end. "Half-open" describes the indices still unchecked,[left, right):rightitself is already known to be True (or past the end), so it stays in the answer range as the fallback, never excluded from it. - The Halting Condition: The loop runs while
left < rightbecause one candidate left (left == right) is already the answer, not becauserightis excluded. At termination,left(andright) points precisely to the boundary index without requiring post-loop fixups. - When to Use: Lower Bound / First Feasible Search (
bisect_left, LC 35, LC 1011 Capacity to Ship Packages, LC 875 Koko Eating Bananas). - Cheat Sheet: positions vs. indices, why
right = len(nums), and the bounds check before readingnums[left]are summarized in Algorithm Cheat Sheet → Binary Search Bounds.
3. Deconstructing "Target": Why the Term Can Mislead
"Target" is an overloaded term in algorithmic problems. Depending on what you are looking for, the invariant changes:
- Exact-Match Target: Looking for a specific key value in an array typically uses closed interval with
left <= rightand 3-way branching (==,<,>). - Insertion Point / Lower Bound Target: Finding the first index where
nums[i] >= targettypically uses half-open withleft < rightandright = mid. - Boundary / Feasibility Target (Answer Space): Finding the minimum valid configuration (capacity, speed, days) where
is_valid(mid)holds uses monotonic predicate boundary searchleft < rightwithright = mid.
4. The "Pair vs. Target" Heuristic (Rule of Thumb)
The popular "Pair vs. Target" trick is a memorable starting heuristic, but it is not a universal law for pointer or binary-search loops:
- The Heuristic: Developers often reach for
<when needing a pair of distinct elements (Two Sum II, Valid Palindrome) becauseleft == rightis degenerate, and<=when testing a target in a closed candidate space so the final singleton is examined. - Why It Is Not Universal: Correct boundary conditions depend on the precise invariant, whether endpoints are inclusive or exclusive , whether a problem needs the first/last valid position or an exact match, and the update rules for
left,right, andmid. For instance, lower-bound and feasibility searches evaluate targets withwhile left < rightandright = midwithout post-processing, while 3-way Dutch National Flag partitioning inspects elements withwhile mid <= high. - Senior Formulation: State your candidate invariant first: "I am maintaining the candidate answer in
[left, right]. Since the candidate set is non-empty wheneverleft <= right, my loop condition iswhile left <= right:, and my updates aremid + 1andmid - 1."
Common Questions
"Binary search is for finding a value in a sorted array. So how does it apply to something like minimizing the maximum ship capacity (LC 1011), where there's no sorted array of capacities to search at all?"
Because sortedness was never the actual requirement — it's just the most familiar disguise of the real one, monotonicity.
Standard search asks "is
nums[mid]too small, too big, or exactly the target?" — three outcomes, and the array's sortedness is what guarantees discarding a half is safe. But binary search only ever needs one yes/no question whose answer flips exactly once as you scan the candidate range:[No, No, No, Yes, Yes, Yes]. A sorted array happens to produce that pattern for "isnums[mid] >= target?" — but so does an entirely invented, unsorted range of candidate answers, as long as the yes/no question you ask about them is monotonic.For "ship packages within
Ddays," the array of package weights is never binary-searched — it's a fixed input, sorted or not. What gets binary-searched is an invented range of possible ship capacities, frommax(weights)tosum(weights). The questioncan_ship(mid)("can a ship of capacitymidfinish inDdays or fewer?") is monotonic: every capacity below the true minimum answersNo, every capacity at or above it answersYes. That single monotonic predicate is the whole requirement — which is why the same algorithm also solves Koko Eating Bananas and Split Array Largest Sum, none of which involve searching a sorted array of anything.
Coach discussion points
- 1.
"What exactly makes this array searchable in
O(log n)? Give me the predicate and prove it never flips back." — Tests whether the student has internalised monotonicity or has merely memorised "sorted array → binary search". - 2.
"You wrote
while lo <= hiandhi = mid. Walk me throughlo == hiand tell me what happens." — Forces the student to discover the infinite loop by tracing rather than being told. - 3.
"In the capacity problem you said
O(log n). What isn, and what does one iteration actually cost?" — Probes whether the predicate's ownO(n)cost was multiplied in. This is the single highest-signal follow-up in this pattern. - 4.
"You have found a match on the first probe and you are about to return. Under what version of this question would returning now be a bug?" — Surfaces the duplicates/boundary distinction, and the difference between "found one" and "found the first".
- 5.
"In the median-of-two-sorted-arrays solution, why do you always binary search the SHORTER array? What changes if you search the longer one instead?" — Tests whether the student understands
j = half - iis a forced arithmetic consequence, not an independent choice, and that searching the shorter array only lowers the cost, fromO(log max(m, n))toO(log min(m, n)). The range[max(0, k - n), min(m, k) + 1)keeps every read inside both lists for either order. - 6.
"Walk me through why
x - arr[mid] > arr[mid + k] - xis monotonic across window left-edges, when neither side is compared to a fixed target." — Probes whether the student can identify monotonicity in a relative, window-shift comparison rather than only in the more familiar "compare to a fixed value" shape. - 7.
"In Search a 2D Matrix you wrote
divmod(mid, n). Which number isn, and what woulddivmod(mid, m)read on a 3 x 4 grid?" — Makes the student find the row-count bug by tracing one flat index, instead of being told that a square test hides it. - 8.
"Search a 2D Matrix II (LC 240) also has sorted rows. Why does the same flat-index search give wrong answers there?" — Tests whether the student knows the flat index needs each row to start after the previous one ends, not just sorted rows.
- 9.
"In Sqrt(x) you wrote
lo = midwhen the check passes. Tracex = 4fromlo = 2, hi = 3withmid = lo + (hi - lo) // 2. What happens, and what one-character change fixes it?" — Makes the student find the floor-midpoint infinite loop of Upper Bound (last True) by tracing, and see why the midpoint must round up once a passingmidis kept withlo = mid. - 10.
"C++ calls its 'first value greater than target' search
upper_bound. Is that a first True or a last True, and how would you get the last index whose value is at mosttarget?" — Separates the library name from the model:upper_bound/bisect_rightis a Lower Bound (first True) search; the last index<= targetis that minus one, or an Upper Bound search. - 11.
"In Separate Squares I,
loandhiare two floats with nothing between them. What does(lo + hi) / 2give, and what doeswhile lo < hido from there?" — Makes the student find the float infinite loop of Continuous Bisection by reasoning about one round, and see why the loop must count rounds. - 12.
"In K-th Smallest Prime Fraction you have
hiafter 100 rounds, but the answer must be a pair. Which fraction do you return, and why can no other fraction be between it andhi?" — Tests the snap: the final window is narrower than the smallest gap between two fractions, about1 / (3 * 104)^2. - 13.
"In Find Minimum in Rotated Sorted Array II,
nums[mid] == nums[hi]. Ismidin the left run or the right one? Show me an array for each answer." — Makes the student build[1, 1, 1, 0, 1]and[1, 0, 1, 1, 1]and see that a tie picks no side, so it can only drophi. - 14.
"Why is dropping
hion a tie safe, and what does that step cost when every value is equal? Can any algorithm do better?" — Tests the invariant (a copy ofnums[hi]stays atmid) and the lower bound: with1s and one hidden0, every unread index could hold the0.
Complexity analysis
Time: O(log n).
Each iteration replaces the live interval with one of its halves, so after k iterations at most n / 2^k candidates remain. The loop ends when that reaches 1, i.e. when k = log₂ n. Formally T(n) = T(n/2) + O(1), which by the Master Theorem (case 2, a = 1, b = 2, f(n) = O(1)) gives Θ(log n).
Concretely: 1,000,000 candidates need at most 20 questions. 1,000,000,000 need at most 30.
Where that changes: in archetype 4.4 the predicate is not O(1) — it is an O(n) greedy scan. Total cost is therefore O(n · log(sum - max)), not O(log n). Students routinely quote O(log n) here and are wrong; the interviewer is checking whether you multiplied by the predicate's own cost.
Space: O(1).
The iterative form holds exactly three integers (lo, hi, mid) plus one accumulator. No allocation, no recursion. The recursive form is O(log n) because each halving pushes a stack frame — a real, if small, difference worth naming aloud.
You are looking up the word "monotone" in a 1,000-page paper dictionary. You do not start at page 1 and turn pages one at a time. You open the book near the middle, land on page 500, read the first word on it, and see it starts with "L". "M" comes after "L", so you have just eliminated pages 1-500 without reading a single one of them. You then repeat on pages 501-1000.
The same shape runs inside real systems every day:
- A PostgreSQL B-Tree index descends toward a row id by comparing keys and discarding subtrees.
- A deployment tool bisecting a broken build (git bisect) halves the commit range on each "is it broken here? yes/no".
- A capacity planner asking "can 2 machines finish this workload in <= T minutes?" and shrinking T.
💡 Don't worry if boundary decisions feel subtle right now! Binary search is one fundamental bisection principle applied across monotonic domains. As you progress through the 9 Archetype Variants (from Exact Match and Rotated Minimum to Capacity Allocation and Median of Two Sorted Arrays), each hands-on challenge will cement this intuition until choosing between lo <= hi vs lo < hi and writing hi = mid - 1 vs hi = mid becomes second nature.
Integer overflow on midpoint calculation: In 32-bit environments (Java/C++/C#),
(left + right) // 2overflows whenleft + right > 231 - 1. Always calculate midpoint asleft + (right - left) // 2.Infinite loop via unreduced interval (
left = mid): Whenright - left = 1, integer division floorsmidtoleft. Assigningleft = midfails to shrink the search space, looping forever. Useleft = mid + 1or half-open intervals.Applying binary search to unsorted inputs: Binary search fundamentally requires a monotonic predicate. Applying it to an unsorted or partially sorted list produces arbitrary incorrect results with no warning.
Staff+ Systems Architecture & Hardware Considerations
Triggers: "sorted order", "monotonic predicate f(x) -> bool", "minimize the maximum / maximize the minimum", "O(log N) runtime budget". Binary search is fundamentally not about looking up numbers in arrays; it is the algorithmic exploitation of monotonicity to eliminate half the remaining candidate solution space in decision steps.
Discrete search space size up to (answer space bisection), or array index boundaries up to . A logarithmic time budget allows traversing an astronomical element space in at most 60 predicate evaluations. Space budget is strictly registers.
Three classic pitfalls: (1) Integer overflow when computing midpoint in 32-bit systems (mitigate via ); (2) Off-by-one infinite loops caused by mismatched loop conditions (while lo < hi vs while lo <= hi) with incorrect boundary assignments; (3) Failure to establish a strictly monotonic boolean predicate.