DSA DAY ONE
DSA DAY ONE
PART I — FOUNDATIONS
Chapter 1: C++ Essentials for DSA
Chapter 2: C++ STL Deep Dive
Chapter 3: Complexity Analysis
PART II — LINEAR DATA STRUCTURES
Chapter 4: Arrays & Strings
Chapter 5: Two Pointers & Sliding Window
Chapter 6: Hashing
Chapter 7: Linked Lists
Chapter 8: Stacks
Chapter 9: Queues & Deques
PART III — TREES
Chapter 10: Binary Trees
Chapter 11: Binary Search Trees (BST)
Chapter 12: Heaps & Priority Queues
Chapter 13: Tries (Prefix Trees)
Chapter 14: Segment Tree & Fenwick Tree
PART IV — ALGORITHMS
Chapter 15: Sorting Algorithms
Chapter 16: Binary Search
Classic Binary Search — The Two Templates`lower_bound` & `upper_bound`Binary Search on the Answer SpaceBinary Search on Rotated Sorted ArraysBinary Search on 2D MatricesCondition-Based Binary Search TemplatesSample ProblemsLeetCode Problem Set
Chapter 17: Recursion & Backtracking
Chapter 18: Graphs — Fundamentals & Traversals
Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 20: Dynamic Programming
Chapter 21: Greedy Algorithms
Chapter 22: Divide & Conquer
PART V — COMPETITIVE & MATH
Chapter 23: Bit Manipulation
Chapter 24: Math & Number Theory
PART VI — PATTERNS
Chapter 25: Pattern Recognition Mastery
PART VII — SYSTEM DESIGN
Chapter 26: System Design for FAANG Interviews
PART VIII — INTERVIEW STRATEGY
Chapter 27: Cracking FAANG — Strategy & Communication
16

Binary Search


Chapter 16 ·  Part 4: ALGORITHMS ·  Est. 28 min read

Chapter Goal: Master binary search in all its forms — not just the classic "find a value in a sorted array" but the far more powerful variants: binary search on the answer space, on rotated arrays, on 2D matrices, and on abstract conditions. Binary search converts O(n) feasibility checks into O(log n) solutions across a wide range of problems that don't look like binary search at first glance.


16.1 Classic Binary Search — The Two Templates

The Core Idea

Binary search eliminates half the search space in every iteration by exploiting a monotone property — the property that once the answer changes from false to true (or true to false), it never changes back.

Sorted array: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]  target=11

lo=0, hi=9:  mid=4, arr[4]=9  < 11 → lo=5
lo=5, hi=9:  mid=7, arr[7]=15 > 11 → hi=6
lo=5, hi=6:  mid=5, arr[5]=11 == 11 → FOUND at index 5

3 iterations vs 10 for linear search. For n=10^9: ~30 vs 10^9.

Prerequisite: The array must be sorted, OR there must exist a monotone condition (property changes from false→true or true→false exactly once).


The Critical Formulation

int mid = lo + (hi - lo) / 2;   // ✅ Always use this — avoids overflow
// NOT: (lo + hi) / 2           — lo+hi can overflow int if both ~2×10^9

Template A — Exact Value Search (while lo <= hi)

Use when: you want to find an exact target and return -1 if absent.

int binarySearch(vector<int>& arr, int target) {
    int lo = 0, hi = (int)arr.size() - 1;
 
    while (lo <= hi) {              // Search space: [lo, hi] (inclusive)
        int mid = lo + (hi-lo)/2;
        if      (arr[mid] == target) return mid;   // Found
        else if (arr[mid] <  target) lo = mid + 1; // Target in right half
        else                         hi = mid - 1; // Target in left half
    }
    return -1;  // Not found
}
// Time: O(log n), Space: O(1)
// Loop exits when lo > hi (empty search space)
// Each iteration: lo=mid+1 or hi=mid-1 — always changes, guaranteed termination

Template B — Boundary Search (while lo < hi)

Use when: you want to find the first/last position satisfying a condition.

// Find first index where arr[idx] >= target  (lower_bound behavior)
int lowerBound(vector<int>& arr, int target) {
    int lo = 0, hi = (int)arr.size();  // hi = n (one past end)
 
    while (lo < hi) {                  // Search space: [lo, hi)
        int mid = lo + (hi-lo)/2;
        if (arr[mid] < target) lo = mid + 1;  // arr[mid] too small → move right
        else                   hi = mid;       // arr[mid] >= target → might be answer
    }
    return lo;  // lo == hi — first index with arr[idx] >= target
                // Returns n if no such element (all elements < target)
}
// Template B key rule: NEVER do hi = mid-1 in this template (loop won't terminate)
// Always: lo = mid+1 (left condition fails), hi = mid (right, potential answer)

Recursive Binary Search

int binarySearch(vector<int>& arr, int target, int lo, int hi) {
    if (lo > hi) return -1;
    int mid = lo + (hi-lo)/2;
    if      (arr[mid] == target) return mid;
    else if (arr[mid] <  target) return binarySearch(arr, target, mid+1, hi);
    else                         return binarySearch(arr, target, lo, mid-1);
}
// Time: O(log n), Space: O(log n) — recursion stack
// Prefer iterative to avoid stack overhead

Off-by-One Errors — The Most Common Bug Source

Template A (lo <= hi):              Template B (lo < hi):
  lo = mid + 1    ✅               lo = mid + 1    ✅
  hi = mid - 1    ✅               hi = mid        ✅ (NOT mid-1!)
  
  DO NOT use hi = mid in A         DO NOT use hi = mid-1 in B
  (can cause infinite loop)        (can skip the answer)
// Demonstrates the infinite loop with hi=mid in Template A:
while (lo <= hi) {
    int mid = lo + (hi-lo)/2;
    if (condition) hi = mid;   // ❌ if lo==hi, mid==lo==hi → hi=lo → no progress
}
 
// Demonstrates why hi=mid works in Template B:
while (lo < hi) {
    int mid = lo + (hi-lo)/2;  // mid < hi always (when lo < hi, mid = (lo+hi)/2 < hi)
    if (condition) hi = mid;   // ✅ hi shrinks, guaranteed termination
}

16.2 lower_bound & upper_bound

These are the two most important binary search variants. Master both.

STL Versions

#include <algorithm>
vector<int> arr = {1, 3, 3, 3, 5, 7, 9};
 
// lower_bound: iterator to FIRST element >= target
auto lb = lower_bound(arr.begin(), arr.end(), 3);
cout << *lb;                          // 3
cout << lb - arr.begin();             // Index: 1
 
// upper_bound: iterator to FIRST element > target
auto ub = upper_bound(arr.begin(), arr.end(), 3);
cout << *ub;                          // 5
cout << ub - arr.begin();             // Index: 4
 
// Count occurrences of target in sorted array:
int count = upper_bound(arr.begin(), arr.end(), 3)
          - lower_bound(arr.begin(), arr.end(), 3);  // 3
 
// equal_range: returns {lower_bound, upper_bound} as a pair
auto [lo, hi] = equal_range(arr.begin(), arr.end(), 3);
int count2 = hi - lo;   // 3
 
// Check if element exists:
bool exists = binary_search(arr.begin(), arr.end(), 3);   // true
 
// Find the closest element to target in sorted array:
int target = 4;
auto it = lower_bound(arr.begin(), arr.end(), target);
// it points to first element >= target (here: 5)
// Check it-1 for closest element < target (here: 3)

Manual Implementations

// lower_bound: returns index of FIRST element >= target (0..n)
int lowerBound(vector<int>& arr, int target) {
    int lo = 0, hi = arr.size();
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (arr[mid] < target) lo = mid + 1;
        else                   hi = mid;
    }
    return lo;  // n if all elements < target
}
 
// upper_bound: returns index of FIRST element > target (0..n)
int upperBound(vector<int>& arr, int target) {
    int lo = 0, hi = arr.size();
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (arr[mid] <= target) lo = mid + 1;  // Only difference: <= instead of <
        else                    hi = mid;
    }
    return lo;  // n if all elements <= target
}

Applications of lower_bound / upper_bound

// 1. Find insertion position (Search Insert Position LC 35)
int searchInsert(vector<int>& arr, int target) {
    return lower_bound(arr.begin(), arr.end(), target) - arr.begin();
}
 
// 2. Count elements in range [a, b] in sorted array
int countInRange(vector<int>& arr, int a, int b) {
    return upper_bound(arr.begin(), arr.end(), b)
         - lower_bound(arr.begin(), arr.end(), a);
}
 
// 3. Find the floor of target (largest element <= target)
int floor(vector<int>& arr, int target) {
    auto it = upper_bound(arr.begin(), arr.end(), target);
    if (it == arr.begin()) return -1;  // No element <= target
    return *prev(it);
}
 
// 4. Find the ceiling of target (smallest element >= target)
int ceiling(vector<int>& arr, int target) {
    auto it = lower_bound(arr.begin(), arr.end(), target);
    if (it == arr.end()) return -1;  // No element >= target
    return *it;
}
 
// 5. Coordinate compression (Chapter 14 recap)
auto getRank = [&](int val, vector<int>& sorted_arr) {
    return lower_bound(sorted_arr.begin(), sorted_arr.end(), val)
           - sorted_arr.begin() + 1;  // 1-indexed rank
};

16.3 Binary Search on the Answer Space

The Key Insight

Instead of searching in the array, search in the space of possible answers.

The Monotone Property for Answer Search: If a candidate answer x satisfies the condition (is feasible), then all values ≥ x (or all ≤ x) also satisfy it. This monotone structure allows binary search on the answer.

Example: "What is the minimum speed such that Koko can eat all bananas in H hours?"

speed=1: feasible? check... (takes many hours > H) → NO
speed=2: feasible? → NO
...
speed=k: feasible? → YES ← minimum speed we want
speed=k+1: YES (also feasible, higher speed)
speed=k+2: YES
...

Pattern: NO NO NO NO YES YES YES YES
                      ^
                 First YES = answer

The Two Master Templates for Answer Search

// ── Template M1: Find MINIMUM value where condition is TRUE ─────────────────
// Use when: "minimize X such that condition(X) is satisfied"
int findMin(int lo, int hi, auto condition) {
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (condition(mid)) hi = mid;       // mid might be the answer → go left
        else                lo = mid + 1;   // mid fails → go right
    }
    return lo;   // Smallest value where condition is true
}
 
// ── Template M2: Find MAXIMUM value where condition is TRUE ─────────────────
// Use when: "maximize X such that condition(X) is satisfied"
int findMax(int lo, int hi, auto condition) {
    while (lo < hi) {
        int mid = lo + (hi-lo+1)/2;   // Round UP to avoid infinite loop!
        if (condition(mid)) lo = mid;       // mid works → go right (try larger)
        else                hi = mid - 1;   // mid fails → go left
    }
    return lo;   // Largest value where condition is true
}
// NOTE: The +1 in mid for Template M2 is CRITICAL.
// Without it: if lo=5, hi=6, mid=5. If condition(5)=true → lo=5 (no change → infinite loop)
// With  it:   if lo=5, hi=6, mid=6. If condition(6)=true → lo=6 (terminates)

Answer Space Example: Koko Eating Bananas

bool canFinish(vector<int>& piles, int speed, int H) {
    long long hours = 0;
    for (int p : piles) hours += (p + speed - 1) / speed;  // ceil(p/speed)
    return hours <= H;
}
 
int minEatingSpeed(vector<int>& piles, int H) {
    int lo = 1;
    int hi = *max_element(piles.begin(), piles.end());  // Maximum possible speed
 
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (canFinish(piles, mid, H)) hi = mid;   // mid might be answer → try smaller
        else                          lo = mid + 1;
    }
    return lo;
}
// Time: O(n log M) where M = max pile size. Space: O(1)

Answer Space Example: Capacity to Ship Packages in D Days

bool canShip(vector<int>& weights, int capacity, int D) {
    int days = 1, load = 0;
    for (int w : weights) {
        if (load + w > capacity) { days++; load = 0; }
        load += w;
    }
    return days <= D;
}
 
int shipWithinDays(vector<int>& weights, int days) {
    int lo = *max_element(weights.begin(), weights.end());  // Min: must fit heaviest
    int hi = accumulate(weights.begin(), weights.end(), 0); // Max: ship all in 1 day
 
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (canShip(weights, mid, days)) hi = mid;
        else                             lo = mid + 1;
    }
    return lo;
}
// Time: O(n log(sum)), Space: O(1)

Choosing the Answer Space Boundaries

lo = minimum possible answer
    Often: 0, 1, or the maximum single element (must be ≥ this to be valid)

hi = maximum possible answer
    Often: sum of all elements, n, or INT_MAX

Condition function: given a candidate answer, return true if it's feasible
    Write it so it runs in O(n) or O(n log n) → total O(n log n) or O(n log²n)

16.4 Binary Search on Rotated Sorted Arrays

The Property

A rotated sorted array like [4, 5, 6, 7, 0, 1, 2] has one key property: at least one half (left or right of mid) is always sorted.

[4, 5, 6, 7, 0, 1, 2]
         mid=7

Left half [4,5,6,7]: is it sorted? 4 <= 7 ✓ → YES
Right half [0,1,2]:  is it sorted? 0 <= 2 ✓ → YES (but could span the wrap)

Actually: check arr[lo] <= arr[mid] to determine if LEFT half is sorted

Searching in a Rotated 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;
 
        // Determine which half is sorted
        if (nums[lo] <= nums[mid]) {
            // LEFT half [lo..mid] is sorted
            if (nums[lo] <= target && target < nums[mid]) {
                hi = mid - 1;   // Target is in the sorted left half
            } else {
                lo = mid + 1;   // Target must be in right half
            }
        } else {
            // RIGHT half [mid..hi] is sorted
            if (nums[mid] < target && target <= nums[hi]) {
                lo = mid + 1;   // Target is in the sorted right half
            } else {
                hi = mid - 1;   // Target must be in left half
            }
        }
    }
    return -1;
}
// Time: O(log n), Space: O(1)
 
/*
  nums = [4,5,6,7,0,1,2], target = 0
  lo=0, hi=6, mid=3, nums[3]=7
    Left sorted? nums[0]=4 <= nums[3]=7 ✓
    0 in [4..7)? 4<=0? No → go right, lo=4
  lo=4, hi=6, mid=5, nums[5]=1
    Left sorted? nums[4]=0 <= nums[5]=1 ✓
    0 in [0..1)? Yes → go left, hi=4
  lo=4, hi=4, mid=4, nums[4]=0 == 0 → return 4 ✓
*/

Finding the Minimum in a Rotated Array (LC 153)

int findMin(vector<int>& nums) {
    int lo = 0, hi = nums.size() - 1;
 
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (nums[mid] > nums[hi]) {
            // Minimum is in the right half (the rotation wrap is there)
            lo = mid + 1;
        } else {
            // Minimum is in the left half (including mid)
            hi = mid;
        }
    }
    return nums[lo];
}
// Time: O(log n), Space: O(1)
 
/*
  nums = [3, 4, 5, 1, 2]
  lo=0, hi=4, mid=2, nums[2]=5 > nums[4]=2 → lo=3
  lo=3, hi=4, mid=3, nums[3]=1 < nums[4]=2 → hi=3
  lo=3=hi → return nums[3]=1 ✓
*/

Rotated Array with Duplicates (LC 81 — Search)

bool searchWithDuplicates(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 true;
 
        // Handle duplicates: can't determine which half is sorted
        if (nums[lo] == nums[mid] && nums[mid] == nums[hi]) {
            lo++; hi--;   // Shrink from both sides
        } else if (nums[lo] <= nums[mid]) {
            if (nums[lo] <= target && target < nums[mid]) hi = mid-1;
            else lo = mid+1;
        } else {
            if (nums[mid] < target && target <= nums[hi]) lo = mid+1;
            else hi = mid-1;
        }
    }
    return false;
}
// Time: O(log n) avg, O(n) worst (all duplicates)

16.5 Binary Search on 2D Matrices

Case 1 — Fully Sorted Matrix (LC 74)

Matrix where each row is sorted AND the first element of row i+1 > last element of row i. This matrix can be treated as a flattened sorted 1D array.

Matrix:                 Flattened:
 1  3  5  7            [1,3,5,7,10,11,16,20,23,30,34,60]
10 11 16 20
23 30 34 60

For any linear index k in [0, m*n-1]:
  row    = k / n
  column = k % n
bool searchMatrix(vector<vector<int>>& matrix, int target) {
    int m = matrix.size(), n = matrix[0].size();
    int lo = 0, hi = m * n - 1;
 
    while (lo <= hi) {
        int mid = lo + (hi-lo)/2;
        int val = matrix[mid/n][mid%n];    // Convert linear index to 2D
 
        if      (val == target) return true;
        else if (val <  target) lo = mid + 1;
        else                    hi = mid - 1;
    }
    return false;
}
// Time: O(log(m×n)), Space: O(1)

Case 2 — Row-Sorted and Column-Sorted Matrix (LC 240)

Each row is sorted left to right, each column top to bottom. Start from top-right corner and eliminate a row or column each step.

bool searchMatrixII(vector<vector<int>>& matrix, int target) {
    int row = 0, col = matrix[0].size() - 1;  // Start: top-right
 
    while (row < (int)matrix.size() && col >= 0) {
        if      (matrix[row][col] == target) return true;
        else if (matrix[row][col] >  target) col--;  // Current too big → go left
        else                                 row++;  // Current too small → go down
    }
    return false;
}
// Time: O(m + n) — each step eliminates a row or column
// NOT pure binary search but uses binary search intuition

Binary Search for Row Then Column

// If only rows are sorted (not the matrix globally):
bool searchRowSorted(vector<vector<int>>& matrix, int target) {
    for (auto& row : matrix) {
        if (binary_search(row.begin(), row.end(), target)) return true;
    }
    return false;
}
// Time: O(m log n) — binary search in each row

16.6 Condition-Based Binary Search Templates

The Four Patterns

Pattern 1: Find FIRST index where arr[i] satisfies condition
  → Template B (lo < hi) with hi = mid when condition is true
  
Pattern 2: Find LAST index where arr[i] satisfies condition
  → Template B (lo < hi) with lo = mid when condition is true, mid rounded UP
  
Pattern 3: Find MINIMUM answer that is feasible
  → Template M1 (lo < hi) with hi = mid when feasible
  
Pattern 4: Find MAXIMUM answer that is feasible
  → Template M2 (lo < hi) with lo = mid when feasible, mid rounded UP

Pattern 1 — Find First True (lower_bound style)

// Find first index i in [0,n) where condition(arr[i]) is TRUE
// Assumption: array is of the form [F,F,...,F,T,T,...,T]
int findFirst(vector<int>& arr, auto condition) {
    int lo = 0, hi = arr.size();
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (condition(arr[mid])) hi = mid;   // Could be answer → search left
        else                     lo = mid+1; // Not answer → search right
    }
    return lo;   // n if no element satisfies condition
}
 
// Example: find first index where arr[i] >= target
auto cond = [&](int x){ return x >= target; };
int idx = findFirst(arr, cond);

Pattern 2 — Find Last True

// Find last index i in [0,n) where condition(arr[i]) is TRUE
// Assumption: array is of the form [T,T,...,T,F,F,...,F]
int findLast(vector<int>& arr, auto condition) {
    int lo = 0, hi = (int)arr.size() - 1;
    int result = -1;
    while (lo <= hi) {
        int mid = lo + (hi-lo)/2;
        if (condition(arr[mid])) { result = mid; lo = mid+1; }  // Found, try right
        else                       hi = mid - 1;
    }
    return result;  // -1 if no element satisfies condition
}

Pattern 3 — Minimize Maximum (Most Common Hard Pattern)

// Split array into k groups, minimize the maximum group sum
// Feasibility: "can we split into k groups each with sum <= capacity?"
bool feasible(vector<int>& arr, int k, long long capacity) {
    long long curr = 0; int groups = 1;
    for (int x : arr) {
        if (curr + x > capacity) { groups++; curr = 0; }
        curr += x;
    }
    return groups <= k;
}
 
int splitArrayLargestSum(vector<int>& nums, int k) {
    long long lo = *max_element(nums.begin(), nums.end());  // Min: must hold largest
    long long hi = accumulate(nums.begin(), nums.end(), 0LL); // Max: one group
 
    while (lo < hi) {
        long long mid = lo + (hi-lo)/2;
        if (feasible(nums, k, mid)) hi = mid;
        else                        lo = mid + 1;
    }
    return lo;
}

Pattern 4 — Maximize Minimum

// Place m elements in n positions, maximize the minimum distance
bool canPlace(vector<int>& pos, int m, int minDist) {
    int count = 1, last = pos[0];
    for (int i = 1; i < (int)pos.size(); i++) {
        if (pos[i] - last >= minDist) { count++; last = pos[i]; }
        if (count >= m) return true;
    }
    return false;
}
 
int maxMinDistance(vector<int>& positions, int m) {
    sort(positions.begin(), positions.end());
    int lo = 1;
    int hi = positions.back() - positions.front();
 
    while (lo < hi) {
        int mid = lo + (hi-lo+1)/2;   // Round UP for "maximize minimum"!
        if (canPlace(positions, m, mid)) lo = mid;
        else                             hi = mid - 1;
    }
    return lo;
}

Binary Search on Non-Sorted Arrays (Find Peak)

Binary search works whenever there is a local monotone condition — not just on globally sorted arrays.

// Find any peak element (element greater than both neighbors) — LC 162
// Key property: if arr[mid] < arr[mid+1], a peak exists in [mid+1, hi]
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;  // Ascending → peak is right
        else                          hi = mid;       // Descending → peak is here or left
    }
    return lo;
}
// Time: O(log n), Space: O(1)
// Works because: nums[-1] = nums[n] = -∞, so a peak must exist

16.7 Sample Problems


Problem 1 — Binary Search (Easy) — 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;
}
// Time: O(log n), Space: O(1)

Problem 2 — Search Insert Position (Easy) — LC 35

Find index where target should be inserted to maintain sorted order.

int searchInsert(vector<int>& nums, int target) {
    int lo = 0, hi = nums.size();   // hi = n (can insert at end)
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (nums[mid] < target) lo = mid + 1;
        else                    hi = mid;
    }
    return lo;   // First index where nums[idx] >= target
}
// Time: O(log n), Space: O(1)
// Equivalent to: lower_bound(nums.begin(), nums.end(), target) - nums.begin()

Problem 3 — Find First and Last Position of Element (Medium) — LC 34

vector<int> searchRange(vector<int>& nums, int target) {
    int first = lower_bound(nums.begin(), nums.end(), target) - nums.begin();
 
    if (first == (int)nums.size() || nums[first] != target) return {-1, -1};
 
    int last = upper_bound(nums.begin(), nums.end(), target) - nums.begin() - 1;
    return {first, last};
}
// Time: O(log n), Space: O(1)

Problem 4 — Search in Rotated Sorted Array (Medium) — 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]) {
            if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
            else lo = mid + 1;
        } else {
            if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
            else hi = mid - 1;
        }
    }
    return -1;
}
// Time: O(log n), Space: O(1)

Problem 5 — Find Minimum in Rotated Sorted Array (Medium) — LC 153

int findMin(vector<int>& nums) {
    int lo = 0, hi = nums.size() - 1;
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        if (nums[mid] > nums[hi]) lo = mid + 1;  // Min is in right half
        else                       hi = mid;       // Min is in left half (including mid)
    }
    return nums[lo];
}
// Time: O(log n), Space: O(1)

Problem 6 — Koko Eating Bananas (Medium) — LC 875

int minEatingSpeed(vector<int>& piles, int h) {
    int lo = 1, hi = *max_element(piles.begin(), piles.end());
 
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        long long hours = 0;
        for (int p : piles) hours += (p + mid - 1) / mid;  // ceil(p/mid)
        if (hours <= h) hi = mid;
        else            lo = mid + 1;
    }
    return lo;
}
// Time: O(n log M) where M = max pile, Space: O(1)

Problem 7 — Capacity to Ship Packages Within D Days (Medium) — LC 1011

int shipWithinDays(vector<int>& weights, int days) {
    int lo = *max_element(weights.begin(), weights.end());
    int hi = accumulate(weights.begin(), weights.end(), 0);
 
    while (lo < hi) {
        int mid = lo + (hi-lo)/2;
        int d = 1, curr = 0;
        for (int w : weights) {
            if (curr + w > mid) { d++; curr = 0; }
            curr += w;
        }
        if (d <= days) hi = mid;
        else           lo = mid + 1;
    }
    return lo;
}
// Time: O(n log(sum)), Space: O(1)

Problem 8 — Search a 2D Matrix (Medium) — LC 74

bool searchMatrix(vector<vector<int>>& matrix, int target) {
    int m = matrix.size(), n = matrix[0].size();
    int lo = 0, hi = m * n - 1;
    while (lo <= hi) {
        int mid = lo + (hi-lo)/2;
        int val = matrix[mid/n][mid%n];
        if      (val == target) return true;
        else if (val <  target) lo = mid + 1;
        else                    hi = mid - 1;
    }
    return false;
}
// Time: O(log(m×n)), Space: O(1)

Problem 9 — Find Peak Element (Medium) — 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;  // Go toward peak
        else                          hi = mid;
    }
    return lo;
}
// Time: O(log n), Space: O(1)

Problem 10 — Split Array Largest Sum (Hard) — LC 410

Split array into at most k subarrays to minimize the largest subarray sum.

int splitArray(vector<int>& nums, int k) {
    long long lo = *max_element(nums.begin(), nums.end());
    long long hi = accumulate(nums.begin(), nums.end(), 0LL);
 
    while (lo < hi) {
        long long mid = lo + (hi-lo)/2;
        // Feasibility: can we split into <= k parts each with sum <= mid?
        long long curr = 0; int parts = 1;
        for (int x : nums) {
            if (curr + x > mid) { parts++; curr = 0; }
            curr += x;
        }
        if (parts <= k) hi = mid;
        else            lo = mid + 1;
    }
    return lo;
}
// Time: O(n log(sum)), Space: O(1)

Problem 11 — Time Based Key-Value Store (Medium) — LC 981

Store key-value pairs with timestamps. Retrieve the value for the largest timestamp ≤ given timestamp.

class TimeMap {
    unordered_map<string, vector<pair<int,string>>> store;  // key → [(timestamp, value)]
 
public:
    void set(string key, string value, int timestamp) {
        store[key].emplace_back(timestamp, value);
        // Note: timestamps are guaranteed increasing, so store is sorted by time
    }
 
    string get(string key, int timestamp) {
        if (!store.count(key)) return "";
        auto& v = store[key];
 
        // Binary search for largest timestamp <= given timestamp
        int lo = 0, hi = (int)v.size();
        while (lo < hi) {
            int mid = lo + (hi-lo)/2;
            if (v[mid].first <= timestamp) lo = mid + 1;
            else                           hi = mid;
        }
        // lo = first index with timestamp > given → lo-1 is the answer
        return lo > 0 ? v[lo-1].second : "";
    }
};
// set: O(1), get: O(log n) where n = number of entries for that key

Problem 12 — Median of Two Sorted Arrays (Hard) — LC 4

Find the median of two sorted arrays in O(log(min(m, n))).

double findMedianSortedArrays(vector<int>& A, vector<int>& B) {
    if (A.size() > B.size()) return findMedianSortedArrays(B, A);  // A is smaller
 
    int m = A.size(), n = B.size();
    int lo = 0, hi = m;
 
    while (lo <= hi) {
        int pa = lo + (hi-lo)/2;        // Partition A: pa elements from A in left half
        int pb = (m+n+1)/2 - pa;        // Partition B: remaining elements needed in left half
 
        int maxLeftA  = (pa == 0) ? INT_MIN : A[pa-1];
        int minRightA = (pa == m) ? INT_MAX : A[pa];
        int maxLeftB  = (pb == 0) ? INT_MIN : B[pb-1];
        int minRightB = (pb == n) ? INT_MAX : B[pb];
 
        if (maxLeftA <= minRightB && maxLeftB <= minRightA) {
            // Correct partition found
            if ((m+n) % 2 == 1)
                return max(maxLeftA, maxLeftB);
            else
                return (max(maxLeftA, maxLeftB) + min(minRightA, minRightB)) / 2.0;
        } else if (maxLeftA > minRightB) {
            hi = pa - 1;   // Too many elements from A in left half
        } else {
            lo = pa + 1;   // Too few elements from A in left half
        }
    }
    return 0.0;
}
// Time: O(log(min(m,n))), Space: O(1)
 
/*
  Key idea: find partition point pa (# elements from A in left half)
  Such that: maxLeft(A) <= minRight(B) AND maxLeft(B) <= minRight(A)
  Then the median is in {max(maxLeftA,maxLeftB), min(minRightA,minRightB)}
 
  A = [1,3], B = [2]  → median = 2
  pa=1, pb=1: maxLeftA=1, minRightA=3, maxLeftB=2, minRightB=MAX
  1 <= MAX ✓, 2 <= 3 ✓ → median = max(1,2) = 2 ✓
*/

16.8 LeetCode Problem Set

# Problem Difficulty Core Technique Link
1 Binary Search Easy Template A (exact value) LC 704
2 Search Insert Position Easy Template B (lower_bound) LC 35
3 Find First and Last Position Medium lower_bound + upper_bound LC 34
4 Search in Rotated Sorted Array Medium Half-sorted binary search LC 33
5 Koko Eating Bananas Medium Binary search on answer (minimize) LC 875
6 Search a 2D Matrix Medium Flatten 2D → 1D binary search LC 74
7 Split Array Largest Sum Hard Binary search on answer (minimize max) LC 410
8 Median of Two Sorted Arrays Hard Binary search on partition LC 4

Suggested order: #1 and #2 (Template A vs B). Then #3 (lower/upper bound applied). Then #4 (rotated — most common medium variant). Then #5 (Koko — the gateway to answer-space binary search). #6 (2D matrix). Save #7 and #8 for last — Split Array Largest Sum is the canonical hard minimize-max problem; Median of Two Sorted Arrays is the hardest binary search in FAANG interviews.


← PreviousChapter 15: Sorting AlgorithmsNext →Chapter 17: Recursion & Backtracking
Chapter 16 — Progress
0 / 22 COMPLETE
Templates
  • Write Template A (exact search, `while lo <= hi`) from memory — update `lo=mid+1` or `hi=mid-1`
  • Write Template B (boundary search, `while lo < hi`) — update `lo=mid+1` or `hi=mid` (NOT mid-1)
  • Know why `mid = lo + (hi-lo)/2` and not `(lo+hi)/2` — overflow prevention
  • Explain when each template is appropriate: A for exact, B for boundary
lower_bound & upper_bound
  • Implement manual lower_bound: condition `arr[mid] < target` → lo=mid+1, else hi=mid
  • Implement manual upper_bound: condition `arr[mid] <= target` → lo=mid+1, else hi=mid
  • Use STL versions: `lower_bound(begin,end,target)` returns iterator to first `>= target`
  • Derive count of target in sorted array: `upper_bound - lower_bound`
Binary Search on Answer Space
  • Write Template M1 (minimize): `if feasible → hi=mid; else lo=mid+1`
  • Write Template M2 (maximize): `mid = lo+(hi-lo+1)/2; if feasible → lo=mid; else hi=mid-1`
  • Know why the +1 in M2's mid formula is critical (avoids infinite loop when lo=hi-1)
  • Identify the lo/hi bounds for the answer space in a problem
  • Write a feasibility check function for Koko Eating Bananas from memory
Rotated Arrays
  • Know the key property: one half is always sorted
  • Determine sorted half: `arr[lo] <= arr[mid]` → left half sorted
  • Search in rotated array: check if target is in the sorted half, else search the other
  • Find minimum: `arr[mid] > arr[hi]` → min in right; else min in left (including mid)
2D Matrix
  • Convert 2D to 1D: `val = matrix[mid/n][mid%n]` for matrix with n columns
  • Know the top-right corner approach for row-and-column-sorted matrix (O(m+n))
Condition-Based Patterns
  • Recognize "minimize maximum" → Template M1 with feasibility check
  • Recognize "maximize minimum" → Template M2 with feasibility check + `mid = lo+(hi-lo+1)/2`
  • Apply to find peak element (non-sorted, but local monotone condition exists)
Notes▸