C++ In Practice - Binary Search Variants
Binary Search Variants (C++11 Deep Dive)
In C++, you should almost never write a raw while (left <= right) loop for standard searches. The STL provides rigorous, optimized, and bug-free implementations in <algorithm>.
STL Binary Search Tools
std::binary_search(begin, end, val): Returnsbool(exists or not).std::lower_bound(begin, end, val): Returns iterator to first element >=val.std::upper_bound(begin, end, val): Returns iterator to first element >val.std::equal_range(begin, end, val): Returns pair of{lower_bound, upper_bound}.
Deep Dive: lower_bound vs upper_bound
- Lower Bound: Finds the start of a range of identical values.
- Upper Bound: Finds the end (technically, the element after the end) of a range.
- Count elements:
upper_bound - lower_bound.
1. First and Last Occurrence (The STL Way)
Problem: Find starting and ending position of target.
Complexity: Time O(\log N) | Space O(1).
#include <vector>
#include <algorithm>
std::vector<int> searchRange(std::vector<int>& nums, int target) {
// 1. Find first occurrence (lower_bound)
auto lb = std::lower_bound(nums.begin(), nums.end(), target);
// Check if target was actually found
if (lb == nums.end() || *lb != target) {
return {-1, -1};
}
// 2. Find upper bound (first element > target)
auto ub = std::upper_bound(nums.begin(), nums.end(), target);
// Calculate indices
// ub points to the element AFTER the last target, so decrement index by 1
return {(int)(lb - nums.begin()), (int)(ub - nums.begin() - 1)};
}
2. Search in Rotated Sorted Array
Problem: Find target in [4,5,6,7,0,1,2].
Logic: One half is always sorted. Determine which half, then check if target lies within that range.
Pitfall: Calculating mid using (left + right) / 2 causes integer overflow when left + right > INT_MAX.
Correct C++: mid = left + (right - left) / 2.
#include <vector>
int searchRotated(std::vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
// Check if Left Half is sorted
if (nums[left] <= nums[mid]) {
// Target is in the left sorted range
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// Otherwise, Right Half must be sorted
else {
// Target is in the right sorted range
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
3. Search a 2D Matrix (Mapping 1D to 2D)
Problem: Matrix sorted row-wise and column-wise (or strictly increasing).
Strictly Increasing: Treat as 1D array.
Mapping: 1D index i -> matrix[i / cols][i % cols].
#include <vector>
bool searchMatrix(std::vector<std::vector<int>>& matrix, int target) {
if (matrix.empty()) return false;
int m = matrix.size();
int n = matrix[0].size();
int left = 0;
int right = m * n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
int row = mid / n;
int col = mid % n;
int val = matrix[row][col];
if (val == target) return true;
if (val < target) left = mid + 1;
else right = mid - 1;
}
return false;
}
4. Find Peak Element
Problem: Find index i where nums[i-1] < nums[i] > nums[i+1].
Pattern: Binary Search on Unsorted Array (Hill Climbing).
Invariant: If nums[mid] < nums[mid+1], we are on an upward slope. The peak must be to the right (or it is mid+1).
#include <vector>
int findPeakElement(std::vector<int>& nums) {
int left = 0;
int right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[mid + 1]) {
// We are on a downward slope. Peak is mid or to the left.
right = mid;
} else {
// We are on an upward slope. Peak is strictly to the right.
left = mid + 1;
}
}
// Loop terminates when left == right
return left;
}
5. Binary Search on Answer: Koko Eating Bananas
Problem: Find min integer k such that condition holds.
Pattern: The condition canFinish(k) is monotonic. False, False, ... True, True. Find the first True.
Integer Math: ceil(pile / k) can be written as (pile + k - 1) / k using integer division to avoid double precision issues.
#include <vector>
#include <algorithm>
#include <cmath>
class Solution {
public:
int minEatingSpeed(std::vector<int>& piles, int h) {
// Range of possible speeds: 1 to Max Pile Size
int left = 1;
int right = *std::max_element(piles.begin(), piles.end());
while (left < right) {
int mid = left + (right - left) / 2;
if (canFinish(piles, h, mid)) {
right = mid; // Try smaller speed
} else {
left = mid + 1; // Need faster speed
}
}
return left;
}
private:
bool canFinish(const std::vector<int>& piles, int h, int k) {
long long hours = 0; // Use long long to prevent overflow
for (int p : piles) {
// Integer ceiling division
hours += (p + k - 1) / k;
}
return hours <= h;
}
};
6. Capacity to Ship Packages
Problem: Min capacity to ship in D days.
Range: [max(weights), sum(weights)].
#include <vector>
#include <numeric> // for std::accumulate
#include <algorithm> // for std::max_element
class Solution {
public:
int shipWithinDays(std::vector<int>& weights, int days) {
int left = *std::max_element(weights.begin(), weights.end());
int right = std::accumulate(weights.begin(), weights.end(), 0);
while (left < right) {
int mid = left + (right - left) / 2;
// Check if capacity 'mid' is feasible
int currentDays = 1;
int currentLoad = 0;
for (int w : weights) {
if (currentLoad + w > mid) {
currentDays++;
currentLoad = 0;
}
currentLoad += w;
}
if (currentDays <= days) {
right = mid; // Feasible, try smaller
} else {
left = mid + 1; // Not feasible, need larger
}
}
return left;
}
};
Summary of Binary Search Patterns
| Problem Type | Search Space | Loop Condition | Update Logic |
|---|---|---|---|
| Exact Value | Array Index | left <= right |
mid+1, mid-1 |
| Boundary (LB) | Array Index | left < right |
right = mid, left = mid+1 |
| Boundary (UB) | Array Index | left < right |
right = mid, left = mid+1 |
| Rotated Array | Array Index | left <= right |
Check sorted half first. |
| Answer Space | Integer Range | left < right |
right = mid (if Valid), left = mid+1 |