Skip to content

Day 13: Binary Search Variations

DAY 13 / 30 · WEEK 2

  • Find the first and last occurrence of a duplicated target — standard binary search only guarantees finding a match, not a specific one.
  • Recognize “search on answer”: binary search over a range of possible answers instead of over array indices, using a yes/no feasibility check at each guess.
  • See two different loop shapes that both narrow toward a boundary instead of stopping at a match: first/last occurrence keeps Day 12’s lo <= hi shape but changes what happens on a match, while search-on-answer switches to a lo &lt; hi convergence loop entirely.

Day 12’s template answers one question: “does this exact value exist, and if so, at which index?” Once you find a match, you stop. But two common situations need more:

Duplicates. If the target appears 4 times in a row, which index does standard binary search return? Whichever one mid happens to land on — not necessarily the first or last. To find a specific boundary, you don’t stop at the first match: you record it as a candidate and keep narrowing in the same direction, discarding the match itself along with everything past it.

Search on answer. Some problems don’t hand you a sorted array at all — they ask “what’s the smallest/largest value that satisfies some condition?” (e.g. “the slowest eating speed that still finishes in time”). If that condition is monotonic — false for every value below some threshold, true for every value at or above it — you can binary search the space of possible answers, testing each guess with a feasibility check instead of an array comparison.

Array [2, 4, 4, 4, 4, 7, 9] — target = 4 appears at indices 1-42444479index 0first=1last=4A plain binary search could land on index 1, 2, 3, or 4 — any of them isa “correct” match. Finding first/last needs two separate, deliberatesearches, each biased to keep narrowing after a match instead of stopping.Green = confirmed boundary. Amber = a match found mid-search, still narrowing.

Interactive visualization: find first and last occurrence

Section titled “Interactive visualization: find first and last occurrence”
current range [lo, hi] mid (checking now) match found, still narrowing eliminated confirmed boundary

Find first and last occurrence of 4 in [2, 4, 4, 4, 4, 7, 9]

Press Play or Step to begin.

Step 0 / 0

function searchRange(nums, target) {
function findBound(isFirst) {
let lo = 0, hi = nums.length - 1, result = -1;
while (lo <= hi) {
const mid = lo + Math.floor((hi - lo) / 2);
if (nums[mid] === target) {
result = mid; // record this match...
if (isFirst) hi = mid - 1; // ...but keep looking further left
else lo = mid + 1; // ...or further right
} else if (nums[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return result;
}
return [findBound(true), findBound(false)];
}

Line by line:

  • The only change from Day 12’s template is the if (nums[mid] === target) branch: instead of return mid immediately, it saves mid as result and then keeps narrowing — toward lo for the first occurrence, toward hi for the last. Every other line is identical to Day 12.
  • Calling the same function twice with a flag (isFirst) avoids duplicating the whole loop — the only difference between the two searches is which side keeps narrowing after a match.

Dry run: findBound(true) — first occurrence of 4 in [2, 4, 4, 4, 4, 7, 9]

Section titled “Dry run: findBound(true) — first occurrence of 4 in [2, 4, 4, 4, 4, 7, 9]”
lo hi mid arr[mid] Action
0 6 3 4 match! result=3, keep looking left → hi=2
0 2 1 4 match! result=1, keep looking left → hi=0
0 0 0 2 2 < 4 → lo=1
1 0 lo > hi, loop ends → return 1

The search keeps going even after finding a match at index 3 — that’s the whole trick. The last confirmed match before the loop ends (index 1) is the true first occurrence.

Koko’s eating speed: is speed v “fast enough to finish in time h”?false — too slowtrue — fast enough to finishflip point = the answerBinary search the speed axis itself, not an array: guess a speed, checkfeasibility in O(piles), narrow toward the flip point in O(log(max pile)).
Variant Time Space Why
First/last occurrence O(log n) O(1) Two independent binary searches over the same n-element array — still logarithmic, just with a factor-of-2 constant.
Search on answer (e.g. Koko) O(n log m) O(1) log m guesses over the answer range (m = largest possible answer), each guess costing O(n) to check feasibility across all n piles.
  • Why does plain binary search fail to find “the first occurrence” reliably? It stops at the first match mid happens to land on, which depends on the exact sequence of midpoints computed — not on where the target actually starts. There is no general rule for which duplicate a plain search returns; treat it as arbitrary.

  • How do you recognize a “search on answer” problem? Look for phrasing like “minimum/maximum value such that…” combined with a check that gets monotonically easier or harder to satisfy as the candidate value increases. If you can write a canDo(candidate) function that’s false then true (or vice versa) as candidate increases, you can binary search candidate directly.
    ↳ Common follow-up: “What if higher isn’t strictly better?” — then the condition isn’t monotonic, and binary search doesn’t apply at all; don’t force it onto a non-monotonic problem.

  • Why does findBound keep Day 12’s lo <= hi shape, but Sqrt(x)/Koko switch to while (lo &lt; hi)? Two different ways to reach a boundary. findBound still checks for an exact value match at each mid — same question as Day 12 — it just keeps going instead of returning immediately, so it keeps Day 12’s inclusive loop shape and tracks the answer in a separate result variable. Sqrt(x)/Koko never check for an exact match at all — they converge lo and hi onto the same index, which needs the exclusive lo &lt; hi loop so the loop stops the instant lo === hi, with no separate result variable needed.
    Trap: mixing the two templates (e.g. while (lo &lt; hi) with hi = mid - 1, or using lo = mid without the ceiling-mid fix from Day 12’s trap diagram) is the single most common way this pattern breaks in an interview.

How to talk through it out loud: name which template you’re using and why — “this needs the leftmost/rightmost boundary, so I’m using the narrowing template, not Day 12’s exact-match one” — before writing a line of code. Interviewers are listening for whether you’re pattern-matching the problem shape to the right template, not just typing something that happens to compile.

Given a non-negative integer x, return the largest integer whose square is ≤ x — without using Math.sqrt.

Given a sorted array with duplicates, return the first and last index of a target value, or [-1, -1] if it’s not present. Today’s main template, applied directly.

Koko has piles of bananas and h hours. Eating at integer speed v bananas/hour, each pile takes ceil(pile / v) hours. Find the minimum speed that finishes all piles within h hours.

  1. When a target appears multiple times, which index does plain binary search (Day 12’s template) return?

    • Whichever index mid happens to land on first — not reliably first or last
    • Always the first occurrence
    • Always the last occurrence
    • Answer: Whichever index mid happens to land on first — not reliably first or last
  2. How does the first-occurrence search change Day 12’s template after finding a match?

    • It records the match and keeps narrowing left instead of returning immediately
    • It restarts the whole search from index 0
    • It stops immediately, same as Day 12
    • Answer: It records the match and keeps narrowing left instead of returning immediately
  3. What property must a “search on answer” feasibility check have?

    • It must be monotonic — false-then-true (or vice versa) as the candidate increases
    • It must run in O(1) time
    • The array of piles must be sorted first
    • Answer: It must be monotonic — false-then-true (or vice versa) as the candidate increases
  4. What’s the time complexity of Koko Eating Bananas’ search-on-answer solution?

    • O(n log m) — n piles checked at each of log m candidate speeds
    • O(log n) — same as a plain array binary search
    • O(n · m) — every speed from 1 to m is tried against every pile
    • Answer: O(n log m) — n piles checked at each of log m candidate speeds
  5. In Sqrt(x)’s solution, why does mid use Math.ceil instead of the usual Math.floor?

    • Because the “feasible” branch sets lo = mid, so mid must round up to avoid freezing at lo
    • Because Math.ceil gives a more numerically precise midpoint
    • There’s no real difference — either would work identically
    • Answer: Because the “feasible” branch sets lo = mid, so mid must round up to avoid freezing at lo