Binary SearchSorted DataO(log n)Search SpaceDivide & Conquer

Binary Search — The Complete Guide

Master Binary Search from first principles. Learn to recognize it instantly, handle tricky boundary conditions, and confidently solve interview questions.

40 min read10 sections
01

Intuition from Scratch

Why does this pattern exist?

Imagine you're looking for a word in a physical dictionary. You don't start at page 1 and read every page — you open to the middle. If your word comes after the middle page, you ignore the entire first half. If it comes before, you ignore the second half. Each step cuts the remaining pages in half. A 1,000-page dictionary takes at most ~10 steps instead of 1,000.

That's Binary Search. It works whenever the data has a monotonic property — something that's consistently true on one side and false on the other. In a sorted array, everything to the left of the target is smaller and everything to the right is larger. This lets you eliminate half the candidates with each comparison, giving O(log n) instead of O(n).

But Binary Search goes far beyond "find a value in a sorted array." Any time you can define a condition that partitions a search space into two halves — one where the answer can't be and one where it might be — you can binary search. This includes searching on the answer itself ("what is the minimum speed such that...").

Analogies to Build Intuition

📖

The Dictionary Lookup

Open to the middle. Is your word before or after? Ignore the wrong half. Repeat. Each step halves the remaining pages. 1,000 pages → 10 steps. That's O(log n).

🎯

The Number Guessing Game

'I'm thinking of a number between 1 and 100. I'll tell you higher or lower.' The optimal strategy? Always guess the middle. You'll find any number in at most 7 guesses (log₂ 100 ≈ 7).

⚖️

The Balance Scale

You have 8 coins, one is heavier. Put 4 on each side of a scale. The heavier side has the fake coin. Split that group of 4 in half. Three weighings find the coin among 8. That's binary search on physical objects.

What kind of problems does it solve?

Binary Search is your go-to when:

  • You need to find a target in a sorted array
  • You need to find the first/last position of a value
  • You need to find the insertion point for a value
  • You need to search on the answer (minimize/maximize a value that satisfies a condition)
  • The problem has a monotonic predicate: false...false, true...true
  • You see an O(log n) time requirement

The core insight

Binary Search works whenever you can define a boolean condition that is false for all values below some threshold and true for all values above it (or vice versa). You're searching for the boundary where the condition flips. The sorted array case is just one instance of this — the condition is "is arr[mid] ≥ target?"

02

Pattern Recognition

Recognizing Binary Search in the wild is the most important skill. The obvious case is "sorted array + find target." The non-obvious case is "minimize the maximum" or "find the smallest value such that..."

Keywords & Signals in the Problem Statement

Use Binary Search when you see...

  • "Sorted array" + find a target or boundary
  • "Search in rotated sorted array"
  • "Find the first/last occurrence of X"
  • "Find minimum/maximum that satisfies a condition"
  • "Koko eating bananas" — search on the answer
  • "Minimum speed/capacity/time such that..."
  • O(log n) time requirement
  • "Find peak element" in a mountain array
  • Monotonic predicate: false...false, true...true
  • Large search space (10⁸+) with a checkable condition

Don't use Binary Search when...

  • Data is unsorted and sorting would lose information
  • You need all elements, not just one — linear scan is unavoidable
  • The search space isn't monotonic — no clear partition
  • The problem is about contiguous subarrays — Sliding Window is better
  • You need O(1) lookup — use a Hash Map
  • The problem involves a graph or tree structure

Binary Search vs. Similar Patterns

PatternWhen to UseKey Difference
Binary SearchSorted data, monotonic predicate, search on answerHalves the search space each step — O(log n)
Two PointersSorted data, find pairs, in-place operationsTwo indices converge — O(n). Doesn't halve the space.
Linear ScanUnsorted data, need to check every elementO(n) — no way to skip elements without sorted order
HashingUnsorted data, need O(1) lookup by valueO(1) lookup but O(n) space. No ordering information.

The 'search on the answer' trick

When a problem says "find the minimum X such that condition(X) is true," and you can check condition(X) in O(n) or less, binary search on X. The search space is [minPossible, maxPossible]. This turns optimization problems into decision problems — much easier to solve.

03

Brute Force → Optimized Thinking

Let's walk through two examples showing the brute-force to binary search evolution.

Example 1: Find Target in Sorted Array

1

Brute Force — Linear Scan

O(n) time · O(1) space

Check every element from left to right. Works but ignores the fact that the array is sorted.

Linear Scantypescript
function searchBrute(nums: number[], target: number): number {
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] === target) return i;
  }
  return -1;
}
// O(n) — doesn't use the sorted property at all.
2

Optimal — Binary Search

O(log n) time · O(1) space

Compare the middle element to the target. If it's too small, search the right half. If too large, search the left half. Each step eliminates half the array.

Binary Searchtypescript
function search(nums: number[], target: number): number {
  let lo = 0, hi = nums.length - 1;

  while (lo <= hi) {
    const mid = lo + Math.floor((hi - lo) / 2); // avoid overflow
    if (nums[mid] === target) return mid;
    if (nums[mid] < target) lo = mid + 1;
    else hi = mid - 1;
  }

  return -1;
}
// O(log n) — halves the search space each step.
// Key: mid = lo + (hi - lo) / 2 avoids integer overflow.

Example 2: Koko Eating Bananas (Search on the Answer)

Koko has piles of bananas and h hours. She eats at speed k bananas/hour (one pile at a time). Find the minimum k such that she finishes all piles in h hours.

1

Brute Force — Try Every Speed

O(max(piles) × n) time

Try k = 1, 2, 3, ... up to max(piles). For each k, check if Koko can finish in h hours. Return the first k that works.

Brute Forcetypescript
function minEatingSpeedBrute(piles: number[], h: number): number {
  for (let k = 1; k <= Math.max(...piles); k++) {
    let hours = 0;
    for (const pile of piles) {
      hours += Math.ceil(pile / k);
    }
    if (hours <= h) return k;
  }
  return Math.max(...piles);
}
// O(max(piles) × n) — tries every possible speed linearly.
2

Optimal — Binary Search on the Answer

O(n × log(max(piles))) time

The key insight: if speed k works, then k+1 also works (monotonic!). So binary search on k in [1, max(piles)]. For each candidate k, check if Koko finishes in ≤ h hours.

Binary Search on the Answertypescript
function minEatingSpeed(piles: number[], h: number): number {
  let lo = 1, hi = Math.max(...piles);

  while (lo < hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    // Can Koko finish at speed mid?
    let hours = 0;
    for (const pile of piles) {
      hours += Math.ceil(pile / mid);
    }
    if (hours <= h) {
      hi = mid;     // mid works — try smaller
    } else {
      lo = mid + 1; // mid too slow — need faster
    }
  }

  return lo;
}
// O(n log(max)) — binary search over speeds, O(n) check per speed.
// The predicate is monotonic: canFinish(k) = false...false, true...true
// We want the FIRST true → use lo < hi template with hi = mid.

Why Does the Optimization Work?

1

The predicate is monotonic

For sorted arrays: 'is arr[mid] ≥ target?' flips from false to true at exactly one point. For Koko: 'can she finish at speed k?' flips from false to true at some threshold. This monotonicity is what makes binary search possible.

2

Each step eliminates half

If the predicate is false at mid, the answer must be in [mid+1, hi]. If true, it's in [lo, mid]. Either way, the search space halves. After log₂(n) steps, only one candidate remains.

3

The general principle

Binary search converts an optimization problem ('find the minimum k') into a decision problem ('can Koko finish at speed k?'). Decision problems are almost always easier to solve. If the decision is monotonic, binary search finds the boundary.

The thinking process

In interviews: "The brute force tries every value linearly. But I notice the predicate is monotonic — once it becomes true, it stays true. So I can binary search for the boundary where it flips. This gives me O(log(range) × check_cost)."

04

Core Templates

Binary Search has three main templates. The difference is in the loop condition and how you update lo/hi. Pick one and stick with it — consistency prevents bugs.

Template 1: Find Exact Target (lo <= hi)

The classic. Search for an exact value. Returns the index or -1.

Exact Target Templatetypescript
function binarySearch(nums: number[], target: number): number {
  let lo = 0, hi = nums.length - 1;

  while (lo <= hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (nums[mid] === target) return mid;
    if (nums[mid] < target) lo = mid + 1;
    else hi = mid - 1;
  }

  return -1; // not found
}
// Loop: lo <= hi (inclusive on both ends)
// Update: lo = mid + 1 or hi = mid - 1 (never mid itself)
// Terminates when: lo > hi (search space empty)
// Use for: "find target", "does target exist?"

Template 2: Find First True (lo < hi)

Find the leftmost position where a condition becomes true. This is the most versatile template — it handles "first occurrence," "lower bound," and "search on the answer."

First True Template (Lower Bound)typescript
function firstTrue(lo: number, hi: number, condition: (mid: number) => boolean): number {
  while (lo < hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (condition(mid)) {
      hi = mid;       // mid satisfies — might be the answer, keep it
    } else {
      lo = mid + 1;   // mid doesn't satisfy — discard it
    }
  }

  return lo; // lo === hi === first position where condition is true
}

// Loop: lo < hi (converge to a single point)
// When condition is true: hi = mid (keep mid as candidate)
// When condition is false: lo = mid + 1 (discard mid)
// Terminates when: lo === hi
// Use for: "first occurrence", "lower bound", "minimum X such that..."
// CRITICAL: condition must be monotonic: false...false, true...true

Template 3: Find Last True (lo < hi with ceiling mid)

Find the rightmost position where a condition is true. Uses ceiling division for mid to avoid infinite loops.

Last True Template (Upper Bound)typescript
function lastTrue(lo: number, hi: number, condition: (mid: number) => boolean): number {
  while (lo < hi) {
    const mid = lo + Math.ceil((hi - lo) / 2); // ceiling!
    if (condition(mid)) {
      lo = mid;       // mid satisfies — might be the answer, keep it
    } else {
      hi = mid - 1;   // mid doesn't satisfy — discard it
    }
  }

  return lo; // lo === hi === last position where condition is true
}

// Key difference: mid uses Math.ceil to avoid infinite loop when lo + 1 === hi
// When condition is true: lo = mid (keep mid as candidate)
// When condition is false: hi = mid - 1 (discard mid)
// Use for: "last occurrence", "upper bound", "maximum X such that..."

Which template to pick?

(1) Need exact match? → Template 1 (lo <= hi). (2) Need first/leftmost boundary? → Template 2 (lo < hi, hi = mid). (3) Need last/rightmost boundary? → Template 3 (lo < hi, lo = mid, ceiling mid). When in doubt, Template 2 is the safest — it handles the most cases.

05

Variations & Sub-Patterns

Binary Search isn't just "find target in sorted array." It's a family of techniques. Here are the most common variations.

VariationWhat ChangesClassic Example
Find exact targetStandard binary search on sorted arrayBinary Search (#704)
Find first/last occurrenceContinue searching after finding targetFirst and Last Position (#34)
Search in rotated arrayOne half is always sorted — determine whichSearch in Rotated Sorted Array (#33)
Find peak elementCompare mid with mid+1 to determine directionFind Peak Element (#162)
Search on the answerBinary search over the answer space, not the arrayKoko Eating Bananas (#875)
Find minimum in rotatedCompare mid with hi to find the rotation pointFind Minimum in Rotated (#153)
Search in 2D matrixTreat the matrix as a flattened sorted arraySearch a 2D Matrix (#74)
Sqrt / nth rootBinary search on integers for the floor of sqrt(x)Sqrt(x) (#69)

Problems mentioned above

Deep Dive: Search in Rotated Sorted Array

The array was sorted then rotated at an unknown pivot. At each step, one half is guaranteed to be properly sorted. Determine which half is sorted, then check if the target falls in that sorted range.

Search in Rotated Sorted Arraytypescript
function search(nums: number[], target: number): number {
  let lo = 0, hi = nums.length - 1;

  while (lo <= hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (nums[mid] === target) return mid;

    // Left half is sorted
    if (nums[lo] <= nums[mid]) {
      if (nums[lo] <= target && target < nums[mid]) {
        hi = mid - 1; // target is in the sorted left half
      } else {
        lo = mid + 1; // target is in the right half
      }
    }
    // Right half is sorted
    else {
      if (nums[mid] < target && target <= nums[hi]) {
        lo = mid + 1; // target is in the sorted right half
      } else {
        hi = mid - 1; // target is in the left half
      }
    }
  }

  return -1;
}
// Key insight: at least one half is always sorted.
// Check if target is in the sorted half. If yes, search there. If no, search the other half.

Deep Dive: Binary Search on the Answer

Instead of searching in an array, you search over the space of possible answers. The pattern: define a predicate canAchieve(x) that's monotonic, then binary search for the boundary.

Search on the Answer — General Patterntypescript
// "Find the minimum X such that condition(X) is true"
function searchOnAnswer(lo: number, hi: number): number {
  while (lo < hi) {
    const mid = lo + Math.floor((hi - lo) / 2);
    if (canAchieve(mid)) {
      hi = mid;     // mid works — try smaller
    } else {
      lo = mid + 1; // mid doesn't work — need larger
    }
  }
  return lo;
}

// Examples of canAchieve:
// - Koko: can she eat all bananas at speed mid in h hours?
// - Ship packages: can we ship all in d days with capacity mid?
// - Split array: can we split into m subarrays with max sum ≤ mid?
// All are O(n) checks, giving O(n log(range)) total.
06

Visual Walkthrough

Let's trace through two examples step by step.

Example 1: Find Target = 7 in Sorted Array

Binary Search — Step-by-Step Tracetext
Array: [1, 3, 5, 7, 9, 11, 13, 15]
Index:  0  1  2  3  4  5   6   7
Target: 7

Step 1: lo=0, hi=7, mid=3
  nums[3] = 7 === target
  Return 3

Done in 1 step! (Best casetarget is at the midpoint)

---

Now let's find target = 11:

Step 1: lo=0, hi=7, mid=3
  nums[3] = 7 < 11lo = 4

Step 2: lo=4, hi=7, mid=5
  nums[5] = 11 === target
  Return 5

Done in 2 steps!

---

Now let's find target = 6 (not in array):

Step 1: lo=0, hi=7, mid=3
  nums[3] = 7 > 6hi = 2

Step 2: lo=0, hi=2, mid=1
  nums[1] = 3 < 6lo = 2

Step 3: lo=2, hi=2, mid=2
  nums[2] = 5 < 6lo = 3

Step 4: lo=3 > hi=2loop ends
  Return -1 (not found)

4 steps for 8 elements. log₂(8) = 3. Worst case islog₂(n)⌋ + 1.

Example 2: Search in Rotated Sorted Array

Rotated Array — Step-by-Step Tracetext
Array: [4, 5, 6, 7, 0, 1, 2]
Index:  0  1  2  3  4  5  6
Target: 0

Step 1: lo=0, hi=6, mid=3
  nums[3] = 70
  Is left half sorted? nums[0]=4 <= nums[3]=7YES
  Is target in [4, 7)? 0 < 4NO
lo = 4 (search right half)

Step 2: lo=4, hi=6, mid=5
  nums[5] = 10
  Is left half sorted? nums[4]=0 <= nums[5]=1YES
  Is target in [0, 1)? 0 >= 0 and 0 < 1YES
hi = 4 (search left half)

Step 3: lo=4, hi=4, mid=4
  nums[4] = 0 === target
  Return 4

Key insight at each step:
- Determine which half is sorted (compare nums[lo] with nums[mid])
- Check if target falls in the sorted range
- If yes, search there. If no, search the other half.
- This works because at least one half is ALWAYS properly sorted.
07

Practice Problems

These 7 problems are carefully ordered from easy to hard. Each one teaches a different aspect of Binary Search. Solve them in order.

1

LC 704. Binary Search

Easy

Why this pattern:

The textbook binary search. Sorted array, find exact target. Use Template 1 (lo <= hi). The simplest possible binary search problem.

Key idea: lo = 0, hi = n-1. While lo <= hi: compute mid, compare with target. If equal, return. If less, lo = mid+1. If greater, hi = mid-1.

2

LC 278. First Bad Version

Easy

Why this pattern:

Find the first position where a predicate becomes true. Classic Template 2 (lo < hi, hi = mid). The predicate is isBadVersion(mid).

Key idea: Binary search for the boundary where isBadVersion flips from false to true. If isBadVersion(mid), hi = mid. Otherwise lo = mid + 1. Return lo.

3

LC 35. Search Insert Position

Easy

Why this pattern:

Find the lower bound — the first index where nums[i] >= target. If target exists, return its index. If not, return where it would be inserted.

Key idea: Template 2: find first index where nums[mid] >= target. If true, hi = mid. If false, lo = mid + 1. Return lo — it's either the target's index or the insertion point.

4

LC 33. Search in Rotated Sorted Array

Medium

Why this pattern:

Modified binary search. At each step, one half is sorted. Determine which half is sorted, check if target is in that range, and search accordingly.

Key idea: If nums[lo] <= nums[mid], left half is sorted. Check if target is in [lo, mid). If yes, hi = mid-1. If no, lo = mid+1. Mirror logic for right half.

5

LC 875. Koko Eating Bananas

Medium

Why this pattern:

Binary search on the answer. The answer space is [1, max(piles)]. For each candidate speed, check if Koko can finish in h hours. Find the minimum speed that works.

Key idea: Binary search over speed k. For each k, compute total hours = sum(ceil(pile/k)). If hours <= h, try smaller k (hi = mid). Otherwise try larger (lo = mid+1).

6

LC 34. Find First and Last Position of Element in Sorted Array

Medium

Why this pattern:

Two binary searches: one for the first occurrence (lower bound) and one for the last occurrence (upper bound). Demonstrates both Template 2 and Template 3.

Key idea: First occurrence: find first index where nums[mid] >= target. Last occurrence: find last index where nums[mid] <= target. Verify both positions actually equal target.

7

LC 4. Median of Two Sorted Arrays

Hard

Why this pattern:

Binary search on the partition point of the smaller array. Find a split where all left elements ≤ all right elements. The median is at this partition boundary.

Key idea: Binary search on the smaller array's partition index. For each partition, check if maxLeft1 <= minRight2 and maxLeft2 <= minRight1. If yes, compute median from the boundary elements.

Practice strategy

For each problem: (1) Identify the search space and the predicate. (2) Verify the predicate is monotonic. (3) Choose the right template. (4) After solving, write: "Search space is [X, Y], predicate is Z, using Template N."

08

Common Mistakes

Binary Search is notorious for off-by-one bugs. These are the traps that catch even experienced developers.

♾️

Infinite loop from wrong mid calculation

Using lo = mid instead of lo = mid + 1 with floor division. When lo + 1 === hi, mid = lo, and lo = mid doesn't advance — infinite loop.

Rule: if you set lo = mid, use ceiling mid (Math.ceil). If you set hi = mid, use floor mid (Math.floor). Or just always use lo = mid + 1 and hi = mid - 1 with Template 1.

📐

Using lo <= hi vs lo < hi incorrectly

Template 1 (exact match) uses lo <= hi. Template 2 (find boundary) uses lo < hi. Mixing them up causes either missed elements or infinite loops.

Pick one template and memorize its loop condition. Template 1: lo <= hi, return inside loop. Template 2: lo < hi, return lo after loop. Don't mix.

🔢

Integer overflow in mid calculation

Using mid = (lo + hi) / 2 when lo + hi exceeds the integer range (relevant in Java/C++, less so in JS/Python).

Always use mid = lo + Math.floor((hi - lo) / 2). This avoids overflow by computing the offset from lo instead of the absolute sum.

🔀

Applying binary search to non-monotonic predicates

The predicate isn't monotonic — it's true, then false, then true again. Binary search can't find the boundary because there isn't a single one.

Before coding, verify: is the predicate monotonic? Does it go false...false, true...true (or vice versa) with exactly one transition? If not, binary search won't work.

🎯

Off-by-one in search bounds

Setting hi = nums.length instead of nums.length - 1 (or vice versa), or returning lo when you should return lo - 1. The answer is off by one position.

Be explicit about whether your bounds are inclusive or exclusive. Template 1: [lo, hi] inclusive. Template 2: [lo, hi) or [lo, hi] — be consistent. Test with a 1-element array.

🏃

Not handling the 'not found' case

Binary search converges to a position, but that position might not contain the target. You return lo without checking if nums[lo] actually equals the target.

After the loop, always verify: does the position actually satisfy your condition? For exact match, check nums[lo] === target. For boundaries, check the value at the returned index.

09

Interview Strategy

Binary Search is a favorite interview topic because it's simple in concept but tricky in implementation. Here's how to nail it.

The 5-Step Interview Flow

1

Clarify & Confirm

'The array is sorted? Can there be duplicates? Do I need the first occurrence, last occurrence, or any occurrence? What should I return if the target doesn't exist?' — These details change which template you use.

2

Identify the Search Space & Predicate

'My search space is [0, n-1] (array indices) / [1, max(piles)] (answer space). My predicate is: nums[mid] >= target / canFinish(mid). The predicate is monotonic because...' — Be explicit.

3

Choose the Template

'I need the exact target → Template 1. I need the first position where condition is true → Template 2. I need the last position → Template 3.' — Name the template.

4

Code It Clean

Write the solution. Use lo/hi/mid (not left/right/center). Add a comment for the predicate. Double-check: does lo = mid+1 or lo = mid? Does the loop use <= or <?

5

Test & Analyze

Trace with a small example AND edge cases: empty array, 1 element, target at start/end, target not present. State complexity: O(log n) time, O(1) space.

Common Follow-Up Questions

Follow-UpHow to Handle
"What if there are duplicates?"Use Template 2 for first occurrence or Template 3 for last occurrence. The key change: don't return immediately when you find target — keep searching.
"What if the array is rotated?"At each step, one half is sorted. Check if target is in the sorted half. If yes, search there. If no, search the other half.
"Can you do it without recursion?"All templates above are iterative. Mention that iterative is preferred in interviews — no stack overflow risk and easier to debug.
"What if the array is infinite?"First, find the upper bound by doubling: start with hi = 1, keep doubling until nums[hi] >= target. Then binary search in [hi/2, hi]. Total: O(log n).
"Can you search on the answer?"Yes — define a monotonic predicate, binary search over the answer space. Examples: minimum speed, maximum capacity, minimum time.
"What about 2D binary search?"For a sorted matrix (each row sorted, first element > last of previous row), treat it as a flattened 1D array. Index mid maps to row = mid/cols, col = mid%cols.

The meta-skill

Interviewers test three things with Binary Search: (1) Can you identify the monotonic property? (2) Can you write bug-free boundary conditions? (3) Can you extend it to non-obvious cases like "search on the answer"? Practice all three — the boundary conditions are where most people fail.

10

Summary & Cheat Sheet

Pin this section. Review it before mock interviews and contests.

Quick Revision Cheat Sheet

Core idea: Halve the search space each step by exploiting a monotonic property — O(n) → O(log n)

Template 1: Exact match: lo <= hi, return inside loop. lo = mid+1, hi = mid-1.

Template 2: First true (lower bound): lo < hi, hi = mid when true, lo = mid+1 when false. Return lo.

Template 3: Last true (upper bound): lo < hi, lo = mid when true, hi = mid-1 when false. Use ceiling mid.

Mid formula: Always: mid = lo + Math.floor((hi - lo) / 2). Avoids integer overflow.

Ceiling mid: For Template 3: mid = lo + Math.ceil((hi - lo) / 2). Prevents infinite loop when lo = mid.

Search on answer: Binary search over [minPossible, maxPossible]. Predicate: canAchieve(mid). Must be monotonic.

Rotated array: One half is always sorted. Check if target is in the sorted half. If yes, search there.

Complexity: Time: O(log n) for array search, O(log(range) × check) for search on answer. Space: O(1).

Mental trigger: "Sorted array" or "minimize/maximize X such that condition" or O(log n) requirement → Binary Search

Decision Flowchart

When to Use Binary Search — Decision Treetext
Is there a MONOTONIC property you can exploit?
├── NONot Binary Search. Consider linear scan, hashing, or DP.
└── YESWhat are you searching for?
          ├── Exact value in sorted arrayTemplate 1 (lo <= hi)
          ├── First position where condition is trueTemplate 2 (lo < hi, hi = mid)
          ├── Last position where condition is trueTemplate 3 (lo < hi, lo = mid, ceil)
          └── Minimum/maximum answer satisfying conditionSearch on Answer (Template 2)

Checklist before coding:
1. What is the search space? [lo, hi]
2. What is the predicate? condition(mid) → boolean
3. Is the predicate monotonic? false...false, true...true?
4. Which template? (exact / first true / last true)
5. What do I return? (lo / hi / mid / -1)

One last thing

Binary Search is deceptively simple — the concept fits in one sentence, but the implementation details (lo vs hi, < vs <=, mid vs mid+1) trip up everyone. The fix is to pick one template, understand exactly why each line exists, and use it consistently. Solve the 7 practice problems in Section 07 with the same template, and the boundary conditions will become second nature.