LC Patterns — Binary Search

Next: two-pointers, sliding-window · Code: C++
Per-pattern template: What it is → Intuition → Approach → Pitfalls → LC problems → Worked examples

◆ The Master Insight

Binary search does not need a sorted array. It needs a monotonic yes/no question: a question over some search space whose answers, read in order, look like FFFF…FTTT…T. Looking something up in a sorted array is just the special case where the question is "is arr[i] ≥ target?"

The one picture: find the F→T boundary
F
F
F
F
T
T
T
T
every pattern below is "find the first Yes" over a different search space: an index, an answer value, a rotation point, a partition line, a version, a real number.
Recognition trigger: "minimum X such that…", "maximum X such that…", "first/last position", or any answer space where if X works, everything on one side of X also works → binary-search the boundary between No and Yes.

§0The Three Templates & Universal Pitfalls

C++ · Template A — exact match (lo ≤ hi)
int lo = 0, hi = n - 1;
while (lo <= hi) {
    int mid = lo + (hi - lo) / 2;
    if (arr[mid] == target) return mid;
    else if (arr[mid] < target) lo = mid + 1;
    else hi = mid - 1;
}
return -1;                          // not found; loop ends with lo > hi
C++ · Template B — boundary search (lo < hi) ← the workhorse, learn cold
int lo = 0, hi = n;                 // hi = n so "no valid index" is representable
while (lo < hi) {
    int mid = lo + (hi - lo) / 2;
    if (predicate(mid)) hi = mid;   // mid is Yes — keep it, it might be the boundary
    else lo = mid + 1;              // mid is No — boundary strictly to the right
}
return lo;                          // lo == hi == first Yes
C++ · Template C — last Yes (lo < hi, right-biased mid)
while (lo < hi) {
    int mid = lo + (hi - lo + 1) / 2;   // RIGHT-biased — required when moving lo = mid
    if (predicate(mid)) lo = mid;       // searching for the LAST Yes
    else hi = mid - 1;
}
return lo;

The 6 ways binary search goes wrong

1. Infinite loop. lo = mid with a floor-mid, or hi = mid with a ceil-mid. Rule: the side that keeps mid must round toward that side.

2. Off-by-one at the boundary. Decide up front: first Yes or last No? Write the invariant as a comment ("everything before lo is No; everything from hi on is Yes") — then the ±1s write themselves.

3. Overflow. (lo+hi)/2 overflows in C++ — always lo + (hi−lo)/2.

4. Wrong initial range. Answer-space searches fail by leaving the true answer outside the range — hi must be a valid upper bound (max(piles), sum(weights)), lo a valid lower bound (max(weights) for capacity, not 1).

5. Non-monotonic question. If the No/Yes pattern isn't one clean boundary, binary search silently returns garbage. Check monotonicity before coding.

6. Empty range / not-found. Decide what lo == n means before you return it.

1Classic Search on Sorted Array

What it is: The textbook case — a sorted array and one target, return its index (or where it would go). Compare the middle to the target, throw away the half that can't contain it, repeat until found or empty.

Binary Search (LC 704) — nums = [−1, 0, 3, 5, 9, 12], target 9
−1
0 lo
0
1
3
2 mid
5
3
9
4 mid₂
12
5 hi
mid=2 → 3 < 9 → go right (lo=3). mid=4 → 9 == 9 → return 4. Two comparisons instead of scanning six.

Intuition: the sorted order is the monotonic question ("is arr[i] ≥ target?"). Halve the space each step → O(log n).

C++ · Binary Search (LC 704)
int search(vector<int>& nums, int target) {
    int lo = 0, hi = nums.size() - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        if (nums[mid] == target) return mid;
        else if (nums[mid] < target) lo = mid + 1;
        else hi = mid - 1;
    }
    return -1;
}

Challenges & pitfalls

None conceptually — this pattern drills the mechanics (mid, bounds, termination) so the later ones are pure thinking. If you make ±1 errors here, drill Template B until you don't. Variants: a sorted API you can only probe (guess number); unknown-size array (double your bounds first, then search).

LC 704 Binary SearchLC 35 Search Insert PositionLC 374 Guess Number Higher or Lower

2First/Last Occurrence (Lower & Upper Bound)

What it is: The array is sorted but the target appears many times (or you want its insertion edge) — find the first or last index satisfying a condition, not just any match. Keep narrowing until the search lands exactly on the edge of the matching block.

Find First and Last Position (LC 34) — nums = [5,7,7,8,8,10], target 8
predicate "value ≥ 8": F F F T T T → lower bound = first T
5
0
7
1
7
2
8
3 first
8
4 last
10
5
lower_bound(8) = 3; upper_bound(8) = 5 → last = 5 − 1 = 4. Answer [3, 4]. Count of 8s = upper − lower = 2.

Intuition: with duplicates, "find target" becomes "find the edge of the target block." First occurrence = first index with arr[i] ≥ target (lower bound); last = upper_bound − 1. This is Template B in its purest form — and it's the foundation: every remaining pattern is a boundary search in disguise.

C++ · Find First and Last Position (LC 34)
int lowerBound(vector<int>& a, int target) {   // first index with a[i] >= target
    int lo = 0, hi = a.size();
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (a[mid] >= target) hi = mid;
        else lo = mid + 1;
    }
    return lo;
}
vector<int> searchRange(vector<int>& a, int target) {
    int first = lowerBound(a, target);
    if (first == (int)a.size() || a[first] != target) return {-1, -1};
    int last = lowerBound(a, target + 1) - 1;   // upper_bound - 1
    return {first, last};
}

Challenges & pitfalls

Resisting the urge to find any occurrence then scan outward (O(n) worst case with duplicates — the interviewer is waiting for it); keeping the two questions straight ( vs >).

LC 34 First and Last PositionLC 278 First Bad VersionLC 744 Smallest Letter Greater Than Target

3Rotated / Modified Sorted Arrays

What it is: A sorted array rotated at an unknown pivot (e.g. [4,5,6,7,0,1,2]) — no longer sorted overall, but still two sorted pieces stuck together. At each step work out which half around mid is properly sorted, then use that half's range to decide where the target could be.

Search in Rotated Sorted Array (LC 33) — nums = [4,5,6,7,0,1,2], target 0
4
0
5
1
6
2
7
3 mid
0
4
1
5
2
6
left half [4..7] is sorted; is 0 in [4,7]? no → search right. Each step, the sorted half tells you definitively where 0 can be → converges to index 4.
sorted halfmid

Intuition: a rotated sorted array is two sorted runs. At any mid, at least one half is fully sorted — figure out which (compare arr[mid] with arr[lo]), check whether the target falls inside that sorted half's range, and go there. You're binary-searching on structure, not raw values.

C++ · Search in Rotated Sorted Array (LC 33)
int search(vector<int>& nums, int target) {
    int lo = 0, hi = nums.size() - 1;
    while (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        if (nums[mid] == target) return mid;
        if (nums[lo] <= nums[mid]) {                       // left half sorted
            if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
            else lo = mid + 1;
        } else {                                          // right half sorted
            if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
            else hi = mid - 1;
        }
    }
    return -1;
}

Challenges & pitfalls

Duplicates break the sorted-half test (arr[lo]==arr[mid]==arr[hi] tells you nothing → shrink by one, lo++/hi--, degrading worst case to O(n) — say this trade-off aloud); comparing against arr[hi] vs arr[lo] gives different edge behavior — pick one; the "is it even rotated?" edge. For the minimum (153): Template B with arr[mid] < arr[hi].

LC 33 Search in RotatedLC 153 Find Minimum in RotatedLC 81 Rotated II (duplicates)

4Peak / Local Structure (search on slope)

What it is: The array isn't sorted at all — it rises and falls like a mountain — and you must find a peak (bigger than both neighbors). You still halve the space because the direction of the slope at mid tells you which side a peak must be on.

Find Peak Element (LC 162) — nums = [1, 2, 3, 1], slope points to a peak
1
0
2
1 mid ↑
3
2 peak
1
3
mid=1: nums[1]=2 < nums[2]=3 → still climbing → a peak is to the right (lo=2). Lands on index 2. No sorted order needed — just the slope.

Intuition: the slope at mid points to a peak. If arr[mid] < arr[mid+1], you're climbing → a peak lies to the right (the climb must turn or hit the end); otherwise a peak is at mid or left. The question "am I past the peak?" is monotonic on a mountain. Generalization: binary search works whenever a local comparison provably lets you throw away half.

C++ · Find Peak Element (LC 162)
int findPeakElement(vector<int>& nums) {
    int lo = 0, hi = nums.size() - 1;
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (nums[mid] < nums[mid + 1]) lo = mid + 1;  // climbing → peak on the right
        else hi = mid;                                // descending → peak here or left
    }
    return lo;
}

Challenges & pitfalls

Convincing yourself (and the interviewer) why throwing away half is safe — the one-liner: "walking toward the ascending side, it either rises until the array ends (the end is a peak) or it turns (the turn is a peak)"; boundary handling at mid+1 — use lo < hi so mid+1 is always in range.

LC 162 Find Peak ElementLC 852 Peak Index in Mountain ArrayLC 1095 Find in Mountain Array

5Binary Search on the Answer (feasibility / min-max) ★ most common interview pattern

What it is: The problem asks for an optimal number — minimum speed, minimum capacity, fewest days, largest size — and there's no array to search at all. You guess a candidate answer, run a cheap "would this work?" check, and binary-search the smallest (or largest) guess that passes. The array is only used inside the checker.

Koko Eating Bananas (LC 875) — piles = [3,6,7,11], h = 8. Search speeds, not the array.
predicate canFinish(speed) over [1..11]: too-slow F … fast-enough T
3
10h ✗
4
8h ✓
5
8h ✓
6
6h ✓
11
4h ✓
faster speed → fewer hours → monotonic FFTT. First Yes = 4. The array only fed the checker.

Intuition: you're usually asked to minimize a maximum (or maximize a minimum): "min eating speed to finish in h hours," "min ship capacity for d days," "min largest subarray sum over k splits." Don't search the array — search the answer space. The check canDo(x) is monotonic: if speed x works, every speed above works too → FFFTTT → find the first Yes.

C++ · Koko Eating Bananas (LC 875)
int minEatingSpeed(vector<int>& piles, int h) {
    int lo = 1, hi = *max_element(piles.begin(), piles.end());
    auto hours = [&](int x) {
        long long t = 0;
        for (int p : piles) t += (p + x - 1) / x;   // ceil(p / x)
        return t;
    };
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (hours(mid) <= h) hi = mid;              // fast enough → try slower
        else lo = mid + 1;
    }
    return lo;
}
The sentence to say: "The answer is monotonic — if capacity x is enough, so is x+1 — so I'll binary-search the answer and write a greedy feasibility check." That one line signals you know the pattern.

Challenges & pitfalls

Spotting that it IS this pattern (trigger words: minimize the maximum, maximize the minimum, least/most X such that, min speed/capacity/days); getting lo right (the subtle bug: lo=1 instead of lo=max(weights) for shipping); the checker must be a correct greedy — that's where the real logic lives, the binary search is trivial.

LC 875 Koko Eating BananasLC 1011 Capacity to Ship PackagesLC 410 Split Array Largest Sum
Example · Capacity to Ship Packages (LC 1011) — weights 1..10, days 5
  • Range: lo = max(weights) = 10 (a day must hold the heaviest box), hi = sum = 55. Checker greedily fills days up to capacity x.
  • Converges to capacity 15. Getting lo = 10 (not 1) is the classic correctness point.

6Value Space with a Counting Function (k-th smallest)

What it is: You need the k-th smallest element in a huge structured collection (a sorted matrix, all pairwise distances) too big to build and sort. Binary-search a candidate value and, using the structure, quickly count how many elements are ≤ it — the smallest value whose count reaches k is the answer.

Kth Smallest in a Sorted Matrix (LC 378) — k = 8, count ≤ x via a staircase
1
5
9
10
11
12
13
13
15
candidate x = 13
count(≤ 13) = 8 first reaches k
→ answer 13
staircase from bottom-left = O(n) per count
binary-search the value range [1, 15]; the sorted structure makes each "how many ≤ x" count cheap. You search values, not positions.

Intuition: a sibling of Pattern 5 for k-th smallest in a structured space. The question count(x) = how many elements ≤ x only goes up as x grows → binary-search the smallest x with count(x) ≥ k. The structure (sorted rows) is what computes count(x) fast without enumerating.

C++ · Kth Smallest in a Sorted Matrix (LC 378)
int kthSmallest(vector<vector<int>>& m, int k) {
    int n = m.size(), lo = m[0][0], hi = m[n-1][n-1];
    auto countLE = [&](int x) {                 // O(n) staircase from bottom-left
        int cnt = 0, r = n - 1, c = 0;
        while (r >= 0 && c < n) {
            if (m[r][c] <= x) { cnt += r + 1; ++c; }
            else --r;
        }
        return cnt;
    };
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (countLE(mid) >= k) hi = mid;
        else lo = mid + 1;
    }
    return lo;
}

Challenges & pitfalls

The mental leap that you binary-search over values that might not exist — it still lands on a real value (the count jumps exactly at data points — be ready to prove it); writing the O(n) counter instead of a lazy O(n log n) one; duplicates in counting (be consistent about vs <).

LC 378 Kth Smallest in Sorted MatrixLC 719 Kth Smallest Pair DistanceLC 668 Kth Smallest in Multiplication Table

7Binary Search on Real Numbers (precision-terminated)

What it is: Same guess-and-check as Pattern 5, but the answer is a real number (a square root, a distance, an average), so there's no exact integer to land on. Halve the interval until it's smaller than the precision you need (or loop a fixed ~100 times) and return the midpoint.

Intuition: same monotonic-question idea, continuous answer. No integer to converge to — stop on precision (hi − lo > eps) or a fixed iteration count (~100 iterations covers any double — the cleaner choice). For integer problems like sqrt, prefer an integer Template C over floats.

C++ · Sqrt(x), integer version (LC 69) — Template C, no floats
int mySqrt(int x) {
    long long lo = 0, hi = x;
    while (lo < hi) {
        long long mid = lo + (hi - lo + 1) / 2;   // right-biased: last mid with mid² ≤ x
        if (mid * mid <= x) lo = mid;
        else hi = mid - 1;
    }
    return lo;
}
// continuous version: for (int it = 0; it < 100; ++it) { double mid = (lo+hi)/2; ... }
Sqrt(8) — last x with x² ≤ 8 (integers)
midmid²≤ 8?move
416nohi = 3
24yeslo = 2
39nohi = 2
lands on 2 = floor(√8). Integers avoid the precision traps a float search invites.

Challenges & pitfalls

eps too tight → time limit; too loose → wrong answer (fixed-iteration sidesteps both — recommend it); floating-point comparison inside the checker (avoid ==); for integer answers, do it in integers.

LC 69 Sqrt(x)LC 367 Valid Perfect SquareLC 774 Minimize Max Distance to Gas Station

8Implicit / Computed Search Space (the array isn't there)

What it is: The values you're searching over are never stored — they're computed from a formula on the index, like "how many numbers are missing before position i." You invent that formula, confirm it's monotonic, and binary-search it exactly as if it were a sorted array.

Kth Missing Positive (LC 1539) — arr = [2,3,4,7,11], k = 5
missing(i) = arr[i] − (i + 1) = how many positives are missing before index i
2
m=1
3
m=1
4
m=1
7
m=3
11
m=6 ≥ 5
first index with missing(i) ≥ 5 is index 4 → answer = k + index = 5 + 4 = 9. (missing positives: 1,5,6,8,9,… → 5th is 9.) We searched a formula, never an actual array.

Intuition: the "array" is defined by a formula, not memory. The missing-count only grows → binary-search over it (1539). Index parity flips right after the single unpaired element (540). A pigeonhole count count(≤ mid) exposes the duplicate (287). You can binary-search any monotonic function of the index — the function stands in for arr[mid].

C++ · Kth Missing Positive (LC 1539)
int findKthPositive(vector<int>& arr, int k) {
    int lo = 0, hi = arr.size();
    while (lo < hi) {                            // first index with missing(i) >= k
        int mid = lo + (hi - lo) / 2;
        if (arr[mid] - (mid + 1) >= k) hi = mid;
        else lo = mid + 1;
    }
    return lo + k;                              // k + number of reals we skipped past
}

Challenges & pitfalls

Finding f — a spot-the-invariant puzzle, different each problem (that's why this feels hard: the search is trivial, the function is the insight); off-by-one in the derived formula (test f on a 4-element example by hand before trusting it).

LC 1539 Kth Missing PositiveLC 540 Single Element in Sorted ArrayLC 287 Find the Duplicate Number

9Partition Between Two Sorted Arrays

What it is: Two sorted arrays, and you need their combined median (or k-th element) without merging. Binary-search where to cut the first array; the size requirement forces the cut in the second, and comparing the four elements around the two cuts tells you whether to slide left or right.

Median of Two Sorted Arrays (LC 4) — find the partition line, not the merge
A (cut after i=1)
1
3
B (cut forced at j=1)
2
+∞
valid cut when A[i−1] ≤ B[j] and B[j−1] ≤ A[i]: 1 ≤ +∞ ✓ and 2 ≤ 3 ✓. Odd total → median = max(left) = 2. No merging — one well-placed cut.

Intuition: the median = the partition line cutting both arrays so the left halves together are half the total and every left element ≤ every right element. Binary-search the cut in the smaller array — the other cut is forced by the size rule. The question "is my cut too far right?" (A[i-1] > B[j]) is monotonic. O(log min(m, n)).

C++ · Median of Two Sorted Arrays (LC 4)
double findMedianSortedArrays(vector<int>& A, vector<int>& B) {
    if (A.size() > B.size()) swap(A, B);            // always search the smaller
    int m = A.size(), n = B.size(), half = (m + n + 1) / 2;
    int lo = 0, hi = m;
    while (lo <= hi) {
        int i = lo + (hi - lo) / 2, j = half - i;   // cut positions (0..m), not indices
        int Al = i ? A[i-1] : INT_MIN, Ar = i < m ? A[i] : INT_MAX;
        int Bl = j ? B[j-1] : INT_MIN, Br = j < n ? B[j] : INT_MAX;
        if (Al <= Br && Bl <= Ar) {
            if ((m + n) & 1) return max(Al, Bl);
            return (max(Al, Bl) + min(Ar, Br)) / 2.0;
        } else if (Al > Br) hi = i - 1;
        else lo = i + 1;
    }
    return 0.0;
}

Challenges & pitfalls

The hardest mechanics in this file — INT_MIN/INT_MAX sentinels for empty sides; searching cut-positions (0..m inclusive), not element indices; always search the smaller array (or j goes negative); odd/even total cases. Nobody gets this cold — write it out twice before any interview where LC 4 could appear.

LC 4 Median of Two Sorted Arrays+ "k-th element of two sorted arrays" (extension)

10Predecessor Search on Versions / Timestamps

What it is: Values written with timestamps or version numbers, and queries ask "what was the value at time t?" — where t may fall between writes. Each key's history is naturally sorted by time, so the query is a binary search for the rightmost entry with timestamp ≤ t.

Time Based Key-Value Store (LC 981) — "foo" history, query at t
t=1
bar
0
t=4
bar2
1
get(3) → rightmost ts ≤ 3 is (1,"bar") → "bar". get(4) → "bar2". get(0) → nothing written → "". Each query = one binary search (upper_bound − 1).

Intuition: systems-flavored — values stored with timestamps; queries ask "the value at time ≤ t" → rightmost element ≤ t = classic (upper_bound − 1) over that key's list. This is Pattern 2 in a system-design costume (it's literally how LSM SSTables and snapshots do reads — nice to mention). Append-only ⇒ the list is sorted for free.

C++ · Time Based Key-Value Store (LC 981)
class TimeMap {
    unordered_map<string, vector<pair<int,string>>> m;   // key → (timestamp, value), increasing
public:
    void set(string key, string value, int timestamp) {
        m[key].push_back({timestamp, value});               // append-only ⇒ sorted for free
    }
    string get(string key, int timestamp) {
        auto& v = m[key];
        int lo = 0, hi = v.size();
        while (lo < hi) {                                    // first index with ts > timestamp
            int mid = lo + (hi - lo) / 2;
            if (v[mid].first <= timestamp) lo = mid + 1;
            else hi = mid;
        }
        return lo == 0 ? "" : v[lo - 1].second;             // step back to rightmost ts ≤ t
    }
};

Challenges & pitfalls

Recognizing "history/snapshot/at time t" = predecessor search; the not-found edge (a query before the first write); in snapshot problems, binary-searching only the changed versions per cell (sparse storage) instead of materializing every snapshot.

LC 981 Time Based Key-Value StoreLC 1146 Snapshot ArrayLC 911 Online Election

11Binary Search as a Subroutine (inside greedy/DP)

What it is: Here binary search isn't the solution — it's the speed-up inside a bigger greedy or DP. You maintain a sorted helper array as you scan (the smallest tails of increasing subsequences, jobs by end time), and each element does one binary search into it, turning an O(n²) loop into O(n log n).

Longest Increasing Subsequence (LC 300) — patience sorting on tails[]
nums = [10, 9, 2, 5, 3, 7, 101, 18] — each element lower_bounds into tails
[10][9][2] [2,5][2,3][2,3,7] [2,3,7,101][2,3,7,18]
tails ends length 4 → LIS length 4. tails isn't the actual subsequence, but its length is the answer; each element did one O(log n) search.

Intuition: binary search is the log-factor accelerator inside another algorithm. Patience sorting for LIS: keep tails[] (the smallest possible tail of an increasing subsequence of each length — provably sorted), each new element binary-searches where it belongs → O(n log n). Weighted job scheduling: a DP where each job binary-searches its latest non-overlapping predecessor.

C++ · Longest Increasing Subsequence (LC 300)
int lengthOfLIS(vector<int>& nums) {
    vector<int> tails;                                 // tails[k] = smallest tail of an LIS of length k+1
    for (int x : nums) {
        auto it = lower_bound(tails.begin(), tails.end(), x);
        if (it == tails.end()) tails.push_back(x);    // x extends the longest run
        else *it = x;                                 // otherwise tighten a tail
    }
    return tails.size();
}

Challenges & pitfalls

Knowing tails[] stays sorted (exchange argument — rehearse the proof); lower_bound vs upper_bound decides strictly-vs-non-strictly increasing (LIS vs longest non-decreasing — a one-character bug); in 2-D versions (envelopes), the sort trick (width ascending, height descending) that reduces it to 1-D LIS.

LC 300 Longest Increasing SubsequenceLC 354 Russian Doll EnvelopesLC 1235 Max Profit in Job Scheduling

Recognition Cheat Sheet

#PatternTrigger phraseSearch spaceQuestion example
1Classic"sorted array, find target"indicesarr[i] ≥ target
2Boundary"first/last occurrence", "count of"indices≥ target vs > target
3Rotated"rotated sorted array"indices"which half is sorted?"
4Peak"peak", "mountain", "local max"indicesarr[i] > arr[i+1]
5BS on answer"min X such that", "minimize the max"answer valuescanDo(x) greedy check
6Value + count"k-th smallest in ⟨structure⟩"value rangecount(≤ x) ≥ k
7Real numberscontinuous answer, "within 10⁻⁶"realsmonotonic check, eps/100-iter
8Implicit array"missing", "single one", "duplicate"indicesa monotonic f(i) you derive
9Two-array partition"median/kth of two sorted arrays"cut positionsA[i-1] ≤ B[j]
10Predecessor/versions"at timestamp t", "snapshot"timestampsrightmost ts ≤ t
11Subroutineinner loop asks a sorted questionmaintained arraylower_bound into an invariant

Study Order & The Interview Sentence

12 (drill until ±1 errors are gone — everything depends on it) → 5 (highest interview frequency) → 3468111079 (hardest mechanics, save for last)
"The question ⟨X⟩ is monotonic over ⟨space⟩ — No then Yes — so I'll binary-search the boundary. Let me state the invariant before coding: everything before lo is No, everything from hi on is Yes."