lo/hi pointers with strict invariant maintenance — always ensure the search space shrinks by at least 1 each iteration.lo + (hi - lo) / 2 instead of (lo + hi) / 2.mid = lo + (hi - lo) / 2. The expression (lo + hi) / 2 caused a bug in Java's Arrays.binarySearch that went undetected for 20 years.while (lo < hi) with lo = mid (not mid + 1), you MUST use ceiling division mid = lo + (hi - lo + 1) / 2 to prevent infinite loops.arr[lo] vs arr[mid] tells you which half is sorted.| Variation | Template Pattern | Time | Space | Key Insight |
|---|---|---|---|---|
| Standard (exact match) | while lo <= hi, return on match | O(log n) | O(1) | Classic — search terminates on exact match or empty range |
| Lower bound (first >=) | while lo < hi, hi = mid | O(log n) | O(1) | Finds leftmost insertion point; equivalent to C++ lower_bound |
| Upper bound (first >) | while lo < hi, lo = mid + 1 | O(log n) | O(1) | Finds rightmost insertion point; equivalent to C++ upper_bound |
| First true (generalized) | while lo < hi, predicate check | O(log n * T(f)) | O(1) | Works on any monotonic predicate, not just arrays |
| Rotated array minimum | while lo < hi, compare mid vs hi | O(log n) | O(1) | Compare with rightmost element, not leftmost |
| Rotated array search | while lo <= hi, check sorted half | O(log n) | O(1) | Identify sorted half first, then check if target is within it |
| Peak element | while lo < hi, compare mid vs mid+1 | O(log n) | O(1) | Move toward the ascending side; works on bitonic sequences |
| Search on answer | while lo < hi, feasibility check | O(log R * T(check)) | O(1) | Binary search the answer, not the array — verify feasibility |
| Matrix search (sorted rows+cols) | Treat as 1D, or row-then-col | O(log(m*n)) | O(1) | Flatten 2D index: row = mid / cols, col = mid % cols |
| Leftmost / rightmost match | Two passes: lower_bound + upper_bound | O(log n) | O(1) | Combine to find count of target: upper - lower |
START
├── Is the search space a sorted array?
│ ├── YES → Need exact match?
│ │ ├── YES → Standard binary search (while lo <= hi)
│ │ └── NO → Need first element >= target?
│ │ ├── YES → Lower bound pattern
│ │ └── NO → Need first element > target?
│ │ ├── YES → Upper bound pattern
│ │ └── NO → Need count of target?
│ │ └── YES → lower_bound + upper_bound combo
│ └── NO ↓
├── Is it a rotated sorted array?
│ ├── YES → Need minimum element?
│ │ ├── YES → Rotated array minimum (compare mid vs hi)
│ │ └── NO → Rotated array search (check sorted half)
│ └── NO ↓
├── Is there a monotonic predicate (false→true or true→false)?
│ ├── YES → Is the predicate on the answer itself?
│ │ ├── YES → Search on answer space
│ │ └── NO → First true / last true pattern
│ └── NO ↓
├── Is the array bitonic (increases then decreases)?
│ ├── YES → Peak element pattern
│ └── NO ↓
├── Is it a sorted 2D matrix?
│ ├── YES → Matrix binary search (flatten to 1D)
│ └── NO ↓
└── DEFAULT → Binary search NOT applicable — use hash map, linear scan, or BFS/DFS
The foundation of all variations. Use while lo <= hi for exact match. [src1]
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
Verify: binary_search([1, 3, 5, 7, 9], 5) → expected: 2
Used for insertion points, counting, and range queries. Never return early — narrow to convergence. [src2]
def lower_bound(arr, target):
lo, hi = 0, len(arr) # hi = len(arr), NOT len(arr)-1
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] < target:
lo = mid + 1
else:
hi = mid
return lo
Verify: lower_bound([1, 3, 5, 5, 7], 5) → expected: 2
Combined with lower bound: range [lower_bound, upper_bound) contains all copies of target. [src2]
def upper_bound(arr, target):
lo, hi = 0, len(arr)
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] <= target: # <= instead of <
lo = mid + 1
else:
hi = mid
return lo
Verify: upper_bound([1, 3, 5, 5, 7], 5) → expected: 4
Determine which half is sorted by comparing arr[lo] vs arr[mid], then check if target lies in the sorted half. [src6]
def search_rotated(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = lo + (hi - lo) // 2
if arr[mid] == target:
return mid
if arr[lo] <= arr[mid]: # left half sorted
if arr[lo] <= target < arr[mid]:
hi = mid - 1
else:
lo = mid + 1
else: # right half sorted
if arr[mid] < target <= arr[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1
Verify: search_rotated([4, 5, 6, 7, 0, 1, 2], 0) → expected: 4
When you can verify if a candidate works but cannot directly compute the answer, binary search the answer itself. [src4]
def search_on_answer(lo, hi, is_feasible):
while lo < hi:
mid = lo + (hi - lo) // 2
if is_feasible(mid):
hi = mid
else:
lo = mid + 1
return lo
Verify: search_on_answer(1, 100, lambda x: x * x >= 50) → expected: 8
For bitonic arrays, compare arr[mid] with arr[mid+1] to decide direction. [src5]
def find_peak(arr):
lo, hi = 0, len(arr) - 1
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] < arr[mid + 1]:
lo = mid + 1
else:
hi = mid
return lo
Verify: find_peak([1, 3, 7, 5, 2]) → expected: 2
# Input: sorted array and target value
# Output: index or insertion point depending on function
from bisect import bisect_left, bisect_right
# Python's standard library provides lower/upper bound:
# bisect_left = lower_bound (first index >= target)
# bisect_right = upper_bound (first index > target)
arr = [1, 3, 5, 5, 5, 7, 9]
target = 5
first_occurrence = bisect_left(arr, target) # 2
last_occurrence = bisect_right(arr, target) - 1 # 4
count = bisect_right(arr, target) - bisect_left(arr, target) # 3
// Input: sorted array, target value
// Output: index of first element >= target
function lowerBound(arr, target) {
let lo = 0, hi = arr.length;
while (lo < hi) {
const mid = lo + ((hi - lo) >> 1);
if (arr[mid] < target) lo = mid + 1;
else hi = mid;
}
return lo;
}
// Search on answer: find minimum capacity to ship within D days
function shipWithinDays(weights, days) {
let lo = Math.max(...weights);
let hi = weights.reduce((a, b) => a + b, 0);
while (lo < hi) {
const mid = lo + ((hi - lo) >> 1);
if (canShip(weights, days, mid)) hi = mid;
else lo = mid + 1;
}
return lo;
}
// Input: sorted int array, target
// Output: index of target, or -1
public static int binarySearch(int[] arr, int target) {
int lo = 0, hi = arr.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // overflow-safe
if (arr[mid] == target) return mid;
else if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
// Rotated array: find minimum element
public static int findMin(int[] arr) {
int lo = 0, hi = arr.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] > arr[hi]) lo = mid + 1;
else hi = mid;
}
return arr[lo];
}
// Input: sorted vector, target value
// Output: iterator to first element >= target
#include <algorithm>
#include <vector>
std::vector<int> v = {1, 3, 5, 5, 7, 9};
auto lb = std::lower_bound(v.begin(), v.end(), 5); // first 5
auto ub = std::upper_bound(v.begin(), v.end(), 5); // points to 7
int count = std::distance(lb, ub); // 2
// Custom predicate with partition_point (C++20):
auto it = std::partition_point(
v.begin(), v.end(),
[&](int x) { return (long long)x * x < n; }
);
// BAD — overflows when lo + hi > Integer.MAX_VALUE
int mid = (lo + hi) / 2;
// GOOD — subtraction cannot overflow when both are non-negative
int mid = lo + (hi - lo) / 2;
// Or in C++20: std::midpoint(lo, hi)
# BAD — when hi - lo == 1, mid == lo, lo never advances
while lo < hi:
mid = lo + (hi - lo) // 2 # floor division
if condition(mid):
hi = mid
else:
lo = mid # INFINITE LOOP
# GOOD — use mid + 1 for lo assignment
while lo < hi:
mid = lo + (hi - lo) // 2
if condition(mid):
hi = mid
else:
lo = mid + 1 # guarantees progress
# BAD — hi should be len(arr) for lower_bound, not len(arr)-1
def lower_bound(arr, target):
lo, hi = 0, len(arr) - 1
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] < target:
lo = mid + 1
else:
hi = mid
return lo # wrong when target > all elements
# GOOD — hi = len(arr) handles "target greater than all" correctly
def lower_bound(arr, target):
lo, hi = 0, len(arr) # half-open [0, n)
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] < target:
lo = mid + 1
else:
hi = mid
return lo # returns len(arr) if target > all
# BAD — comparing mid with lo for minimum finding
def find_min_rotated(arr):
lo, hi = 0, len(arr) - 1
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] > arr[lo]: # wrong comparison
lo = mid + 1
else:
hi = mid
return arr[lo] # fails on [2, 1]
# GOOD — comparing with hi correctly identifies minimum half
def find_min_rotated(arr):
lo, hi = 0, len(arr) - 1
while lo < hi:
mid = lo + (hi - lo) // 2
if arr[mid] > arr[hi]: # min in right half
lo = mid + 1
else:
hi = mid
return arr[lo] # correctly returns 1 for [2, 1]
(lo + hi) exceeds Integer.MAX_VALUE for large arrays. Fix: mid = lo + (hi - lo) / 2. This affected production code for decades. [src3]while lo < hi when you need while lo <= hi either misses the last candidate or causes infinite loops. Fix: Use <= for exact match (return inside loop), < for convergence (return after loop). [src2]len(arr) when target exceeds all elements — not checking causes index-out-of-bounds. Fix: Always check if lo < len(arr) and arr[lo] == target. [src5]lo = mid: When hi = lo + 1, floor division gives mid = lo, and lo = mid makes no progress. Fix: Use lo = mid + 1 or ceiling division. [src1]arr[lo] == arr[mid] == arr[hi]. Fix: Skip duplicates with lo++ or hi--. [src6]| Use When | Don't Use When | Use Instead |
|---|---|---|
| Data is sorted or has a monotonic property | Data is unsorted with no ordering | Hash table O(1) or linear scan |
| Need O(log n) search in a static sorted array | Array is frequently modified | Balanced BST or skip list |
| Minimizing/maximizing with a feasibility check | Feasibility function is not monotonic | Dynamic programming or greedy |
| Finding boundaries in sorted data | Need all matching elements | Two binary searches + iteration |
| Search space is very large (10^9+) | Predicate is expensive and n is small | Linear scan may be faster |
| Finding peak/valley in bitonic sequences | Array has multiple peaks | Segment tree or linear scan |
while lo <= hi, return inside loop) and boundary finding (while lo < hi, return after loop). Mixing conventions is the #1 source of bugs.