Master binary search completely — the core algorithm with loop invariant proof, iterative and recursive implementations, all variants (leftmost, rightmost, lower bound, upper bound), binary search on answer space, and 10 classic interview problems.
What Is Binary Search?
Binary search finds a target in a sorted array by repeatedly halving the search range. At each step, it compares the target with the middle element and eliminates half the remaining candidates.
It is one of the most important algorithms in computer science — simple in concept, powerful in practice, and subtle in correct implementation.
Loop Invariant — Proof of Correctness
The loop invariant is: "If target exists in the array, it must be in arr[left..right]."
- Initialization: Before the loop, left=0 and right=n-1, so the invariant holds (entire array).
- Maintenance: After each iteration, we narrow the range to the half where target can be, preserving the invariant.
- Termination: When left > right, the search space is empty. By the invariant, target is not in the array → return -1.
This invariant guarantees the algorithm is correct for any sorted input.
Step-by-Step Trace
| Iter 1 | left=0, right=10, mid=5 |
| arr[5]=23 == target | return 5 ✓ |
| Iter 1: left=0, right=10, mid=5 arr[5]=23 < 38 | left=6 |
| Iter 2: left=6, right=10, mid=8 arr[8]=56 > 38 | right=7 |
| Iter 3: left=6, right=7, mid=6 arr[6]=38 == 38 | return 6 ✓ |
| Iter 1: left=0, right=10, mid=5 arr[5]=23 < 30 | left=6 |
| Iter 2: left=6, right=10, mid=8 arr[8]=56 > 30 | right=7 |
| Iter 3: left=6, right=7, mid=6 arr[6]=38 > 30 | right=5 |
| left(6) > right(5) | return -1 ✓ |
Recursive Implementation
def binary_search_recursive(arr, target, left=None, right=None):
"""O(log n) time, O(log n) space (call stack)."""
if left is None: left, right = 0, len(arr) - 1
if left > right: return -1 # Base case: empty search space
mid = left + (right - left) // 2
if arr[mid] == target: return mid
elif arr[mid] < target: return binary_search_recursive(arr, target, mid+1, right)
else: return binary_search_recursive(arr, target, left, mid-1)
Prefer iterative — avoids O(log n) stack space.
Complexity Analysis
| Initial search space | n elements |
| After 1 comparison | n/2 elements |
| After 2 comparisons | n/4 elements |
| After k comparisons | n/2^k elements |
| n < 2^k | k > log₂(n) |
| Maximum iterations | ⌊log₂(n)⌋ + 1 |
| For n = 1,000,000 | at most 20 comparisons |
| For n = 1,000,000,000 | at most 30 comparisons |
Time: O(log n) | Space: O(1) iterative, O(log n) recursive
All Binary Search Variants
Find Leftmost (First) Occurrence
def leftmost(arr, target):
"""Find index of FIRST occurrence. Returns -1 if not found."""
left, right = 0, len(arr) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
result = mid # Record this position
right = mid - 1 # Keep searching LEFT for earlier occurrence
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
arr = [1, 2, 2, 2, 3, 4, 5]
print(leftmost(arr, 2)) # 1 (index of first 2)
Find Rightmost (Last) Occurrence
def rightmost(arr, target):
"""Find index of LAST occurrence. Returns -1 if not found."""
left, right = 0, len(arr) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
result = mid # Record this position
left = mid + 1 # Keep searching RIGHT for later occurrence
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
print(rightmost(arr, 2)) # 3 (index of last 2)
Count Occurrences
def count_occurrences(arr, target):
first = leftmost(arr, target)
if first == -1: return 0
last = rightmost(arr, target)
return last - first + 1
arr = [1, 2, 2, 2, 3, 4, 5]
print(count_occurrences(arr, 2)) # 3
print(count_occurrences(arr, 6)) # 0
Lower Bound — First index where arr[i] >= target
def lower_bound(arr, target):
"""
First position where arr[i] >= target.
If all elements < target, returns len(arr) (insert at end).
"""
left, right = 0, len(arr) # Note: right = len(arr), not len(arr)-1
while left < right: # Note: strict <, not <=
mid = left + (right - left) // 2
if arr[mid] < target:
left = mid + 1
else:
right = mid
return left # Index of first element >= target
arr = [1, 3, 5, 7, 9]
print(lower_bound(arr, 5)) # 2 (arr[2]=5 is first >=5)
print(lower_bound(arr, 6)) # 3 (arr[3]=7 is first >=6)
print(lower_bound(arr, 10)) # 5 (insert after all)
print(lower_bound(arr, 0)) # 0 (insert before all)
Upper Bound — First index where arr[i] > target
def upper_bound(arr, target):
"""First position where arr[i] > target."""
left, right = 0, len(arr)
while left < right:
mid = left + (right - left) // 2
if arr[mid] <= target: # Note: <= instead of <
left = mid + 1
else:
right = mid
return left
arr = [1, 3, 5, 5, 5, 7, 9]
print(lower_bound(arr, 5)) # 2 (first 5)
print(upper_bound(arr, 5)) # 5 (after last 5)
# Count of 5s: upper_bound - lower_bound = 5 - 2 = 3 ✓
Binary Search on Answer Space
A powerful technique: binary search over the *answer* rather than over array indices.
Pattern: When you can verify whether a candidate answer x is feasible/valid in O(f(n)) time, and feasibility is monotone (all values ≤ x are feasible, or all ≥ x are feasible), binary search on x.
def min_speed_to_arrive_on_time(dist, hour):
"""
Find minimum integer speed to complete all train trips by 'hour'.
Each trip takes ceil(dist[i]/speed) hours except the last.
Binary search on speed from 1 to max(dist).
"""
import math
def can_arrive(speed):
total = 0
for i in range(len(dist) - 1):
total += math.ceil(dist[i] / speed) # Round up each trip
total += dist[-1] / speed # Last trip: no rounding
return total <= hour
if hour <= len(dist) - 1:
return -1 # Impossible: need at least 1 hour per trip before last
left, right = 1, max(dist)
while left < right:
mid = left + (right - left) // 2
if can_arrive(mid):
right = mid # Speed mid works, try lower
else:
left = mid + 1 # Speed mid too slow, need faster
return left
Python's bisect Module
Python provides built-in binary search via bisect:
import bisect
arr = [1, 3, 5, 7, 9]
# Lower bound: first index >= target
print(bisect.bisect_left(arr, 5)) # 2 (arr[2]=5)
print(bisect.bisect_left(arr, 6)) # 3 (arr[3]=7, first >=6)
# Upper bound: first index > target
print(bisect.bisect_right(arr, 5)) # 3 (first index after all 5s)
print(bisect.bisect(arr, 5)) # Same as bisect_right
# Insert maintaining sort order:
bisect.insort_left(arr, 4) # arr becomes [1,3,4,5,7,9]
bisect.insort(arr, 6) # arr becomes [1,3,4,5,6,7,9]
Classic Binary Search Interview Problems
1 — Search in Rotated Sorted Array
def search_rotated(arr, target):
"""Binary search in sorted array that was rotated. O(log n)."""
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target: return mid
if arr[left] <= arr[mid]: # Left half is sorted
if arr[left] <= target < arr[mid]:
right = mid - 1
else:
left = mid + 1
else: # Right half is sorted
if arr[mid] < target <= arr[right]:
left = mid + 1
else:
right = mid - 1
return -1
print(search_rotated([4,5,6,7,0,1,2], 0)) # 4
2 — Find Peak Element
def find_peak(nums):
"""Find any peak element (greater than both neighbors). O(log n)."""
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] < nums[mid+1]:
left = mid + 1 # Peak is to the right
else:
right = mid # Peak is at mid or to the left
return left
print(find_peak([1,2,3,1])) # 2 (nums[2]=3 is a peak)
print(find_peak([1,2,1,3,5,6,4])) # 5 (nums[5]=6 is a peak)
3 — Find Minimum in Rotated Sorted Array
def find_min_rotated(nums):
"""Find minimum element in rotated sorted array. O(log n)."""
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[right]:
left = mid + 1 # Minimum is in right half
else:
right = mid # Minimum is at mid or in left half
return nums[left]
print(find_min_rotated([3,4,5,1,2])) # 1
print(find_min_rotated([4,5,6,7,0,1,2])) # 0
Summary
Binary search: O(log n) on sorted arrays.
Core: Compare with middle, discard half, repeat until found or empty.
Variants (all O(log n)):
- Exact match: standard binary search
- First/last occurrence: adjust to keep searching after finding
- Lower/upper bound: find insertion points
- Answer space: binary search over feasibility
Key implementation details:
- Use
left + (right - left) // 2 for mid (overflow-safe) - Distinguish
left <= right (exact match) vs left < right (bound finding) - Always verify: when does
left = mid + 1 vs right = mid - 1 vs right = mid?
*Next Lesson: Interpolation Search*