374 lines
11 KiB
YAML
374 lines
11 KiB
YAML
name: Binary Search
|
|
slug: binary-search
|
|
difficulty_level: 2
|
|
pattern_type: algorithm
|
|
display_order: 4
|
|
|
|
description: >
|
|
Efficiently search sorted data by repeatedly dividing the search space in half.
|
|
This transforms O(n) linear search into O(log n) by eliminating half the
|
|
remaining possibilities with each comparison.
|
|
|
|
when_to_use: |
|
|
- Sorted arrays or search spaces
|
|
- Finding boundaries (first/last occurrence)
|
|
- Searching in rotated sorted arrays
|
|
- Finding peak elements
|
|
- Minimizing/maximizing with monotonic constraints
|
|
|
|
metaphor: |
|
|
Imagine playing a number guessing game where someone says "higher" or "lower"
|
|
after each guess. The optimal strategy is always guessing the middle—you
|
|
eliminate half the possibilities each time regardless of the answer.
|
|
|
|
Another analogy: looking up a word in a physical dictionary. You don't read
|
|
page by page from the start. You open roughly to the middle, see if you're
|
|
before or after your word, then repeat in the appropriate half.
|
|
|
|
core_concept: |
|
|
Binary search works because the data has **monotonic ordering**—if you find
|
|
something too small, everything before it is also too small. If something is
|
|
too big, everything after is also too big.
|
|
|
|
The key insight extends beyond simple arrays:
|
|
|
|
1. **Value search**: Find a specific target in sorted array
|
|
2. **Boundary search**: Find the first/last element satisfying a condition
|
|
3. **Search space**: Binary search over answers (e.g., "minimum capacity")
|
|
|
|
At each step, you make one comparison and eliminate half the space. After k
|
|
comparisons, you've narrowed n elements down to n/2^k. Solving n/2^k = 1
|
|
gives k = log₂(n).
|
|
|
|
visualization: |
|
|
**Example: Find target = 7 in sorted array**
|
|
|
|
```
|
|
Array: [1, 3, 5, 7, 9, 11, 13]
|
|
L M R
|
|
|
|
Step 1: mid = 7, target = 7 → Found!
|
|
```
|
|
|
|
**Example: Find target = 9**
|
|
|
|
```
|
|
[1, 3, 5, 7, 9, 11, 13]
|
|
L M R
|
|
|
|
Step 1: mid = 7 < 9 → Search right half
|
|
L M R
|
|
|
|
Step 2: mid = 11 > 9 → Search left half
|
|
L
|
|
M
|
|
R
|
|
|
|
Step 3: mid = 9 → Found at index 4!
|
|
```
|
|
|
|
**Binary search on answer: Minimum capacity to ship packages in D days**
|
|
|
|
```
|
|
Search space: [max(weights), sum(weights)]
|
|
|
|
mid = some capacity
|
|
Can ship in D days with capacity mid?
|
|
Yes → try smaller capacity (go left)
|
|
No → need more capacity (go right)
|
|
```
|
|
|
|
code_template: |
|
|
def binary_search(arr: list, target: int) -> int:
|
|
"""Classic binary search for exact match."""
|
|
left, right = 0, len(arr) - 1
|
|
|
|
while left <= right:
|
|
mid = left + (right - left) // 2 # Avoid overflow
|
|
|
|
if arr[mid] == target:
|
|
return mid
|
|
elif arr[mid] < target:
|
|
left = mid + 1
|
|
else:
|
|
right = mid - 1
|
|
|
|
return -1 # Not found
|
|
|
|
|
|
def lower_bound(arr: list, target: int) -> int:
|
|
"""Find first position where arr[i] >= target."""
|
|
left, right = 0, len(arr)
|
|
|
|
while left < right:
|
|
mid = left + (right - left) // 2
|
|
|
|
if arr[mid] < target:
|
|
left = mid + 1
|
|
else:
|
|
right = mid
|
|
|
|
return left # First valid position
|
|
|
|
|
|
def binary_search_answer(low: int, high: int, is_valid) -> int:
|
|
"""Binary search on answer space."""
|
|
while low < high:
|
|
mid = low + (high - low) // 2
|
|
|
|
if is_valid(mid):
|
|
high = mid # Try smaller
|
|
else:
|
|
low = mid + 1 # Need bigger
|
|
|
|
return low
|
|
|
|
recognition_signals:
|
|
- "sorted array"
|
|
- "O(log n)"
|
|
- "find minimum/maximum"
|
|
- "find first/last occurrence"
|
|
- "rotated sorted array"
|
|
- "peak element"
|
|
- "minimum capacity"
|
|
- "search space"
|
|
- "lower bound"
|
|
- "upper bound"
|
|
|
|
common_mistakes:
|
|
- title: Integer overflow in mid calculation
|
|
description: |
|
|
Using `(left + right) / 2` can overflow if left and right are both
|
|
large positive integers (in languages with fixed-size integers).
|
|
fix: |
|
|
Use `left + (right - left) // 2` instead. This is mathematically
|
|
equivalent but avoids overflow.
|
|
|
|
- title: Infinite loop with wrong boundary update
|
|
description: |
|
|
Using `right = mid` with `left <= right` condition, or `left = mid`
|
|
when mid could equal left, causes infinite loops.
|
|
fix: |
|
|
For `left <= right`, always use `left = mid + 1` and `right = mid - 1`.
|
|
For `left < right`, use `left = mid + 1` and `right = mid`.
|
|
|
|
- title: Off-by-one with boundary search
|
|
description: |
|
|
Returning `left` when you should return `left - 1` (or vice versa)
|
|
gives the wrong boundary element.
|
|
fix: |
|
|
Think carefully about loop invariants. What does `left` represent when
|
|
the loop ends? Test with edge cases.
|
|
|
|
- title: Not recognizing binary search applicability
|
|
description: |
|
|
Missing that a problem can use binary search because the "array" is
|
|
implicit (search space of possible answers).
|
|
fix: |
|
|
If you need to minimize/maximize something and can write a function
|
|
`is_valid(x)` that's monotonic, binary search applies.
|
|
|
|
variations:
|
|
- name: Classic search
|
|
description: |
|
|
Find exact target in sorted array. Returns index or -1.
|
|
example: "Binary Search, Search Insert Position"
|
|
|
|
- name: Lower/Upper bound
|
|
description: |
|
|
Find first or last position satisfying a condition. Used for ranges
|
|
and counting occurrences.
|
|
example: "First Bad Version, Find First and Last Position"
|
|
|
|
- name: Rotated array
|
|
description: |
|
|
Sorted array rotated at some pivot. One half is always sorted—determine
|
|
which half and search appropriately.
|
|
example: "Search in Rotated Sorted Array, Find Minimum"
|
|
|
|
- name: Binary search on answer
|
|
description: |
|
|
Search the space of possible answers. Need a monotonic predicate function
|
|
to determine feasibility.
|
|
example: "Capacity To Ship Packages, Koko Eating Bananas, Split Array Largest Sum"
|
|
|
|
- name: Peak finding
|
|
description: |
|
|
Find local maximum in bitonic array. Compare mid with neighbors to
|
|
determine which side has the peak.
|
|
example: "Find Peak Element, Find in Mountain Array"
|
|
|
|
related_patterns:
|
|
- two-pointers
|
|
|
|
prerequisite_patterns: []
|
|
|
|
visualization_examples:
|
|
- id: binary-search-find-target
|
|
title: Find Target in Sorted Array
|
|
input:
|
|
nums: [1, 3, 5, 7, 9, 11, 13]
|
|
target: 9
|
|
code: |
|
|
def binary_search(nums, target):
|
|
left, right = 0, len(nums) - 1
|
|
|
|
while left <= right:
|
|
mid = left + (right - left) // 2
|
|
|
|
if nums[mid] == target:
|
|
return mid
|
|
elif nums[mid] < target:
|
|
left = mid + 1
|
|
else:
|
|
right = mid - 1
|
|
|
|
return -1
|
|
steps:
|
|
- id: step-1
|
|
description: "Initialize pointers: left=0, right=6. Search space is entire array."
|
|
structures:
|
|
nums:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: default }
|
|
- { value: 3, state: default }
|
|
- { value: 5, state: default }
|
|
- { value: 7, state: default }
|
|
- { value: 9, state: default }
|
|
- { value: 11, state: default }
|
|
- { value: 13, state: default }
|
|
pointers:
|
|
left: 0
|
|
right: 6
|
|
variables:
|
|
target: 9
|
|
left: 0
|
|
right: 6
|
|
codeHighlight:
|
|
startLine: 2
|
|
endLine: 2
|
|
|
|
- id: step-2
|
|
description: "Calculate mid=3. nums[3]=7 < target=9, so search right half."
|
|
structures:
|
|
nums:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: visited }
|
|
- { value: 3, state: visited }
|
|
- { value: 5, state: visited }
|
|
- { value: 7, state: comparing }
|
|
- { value: 9, state: default }
|
|
- { value: 11, state: default }
|
|
- { value: 13, state: default }
|
|
pointers:
|
|
left: 0
|
|
mid: 3
|
|
right: 6
|
|
variables:
|
|
target: 9
|
|
left: 0
|
|
right: 6
|
|
mid: 3
|
|
codeHighlight:
|
|
startLine: 9
|
|
endLine: 10
|
|
|
|
- id: step-3
|
|
description: "Update left=4. New search space is indices 4-6."
|
|
structures:
|
|
nums:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: visited }
|
|
- { value: 3, state: visited }
|
|
- { value: 5, state: visited }
|
|
- { value: 7, state: visited }
|
|
- { value: 9, state: default }
|
|
- { value: 11, state: default }
|
|
- { value: 13, state: default }
|
|
pointers:
|
|
left: 4
|
|
right: 6
|
|
variables:
|
|
target: 9
|
|
left: 4
|
|
right: 6
|
|
codeHighlight:
|
|
startLine: 4
|
|
endLine: 4
|
|
|
|
- id: step-4
|
|
description: "Calculate mid=5. nums[5]=11 > target=9, so search left half."
|
|
structures:
|
|
nums:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: visited }
|
|
- { value: 3, state: visited }
|
|
- { value: 5, state: visited }
|
|
- { value: 7, state: visited }
|
|
- { value: 9, state: default }
|
|
- { value: 11, state: comparing }
|
|
- { value: 13, state: visited }
|
|
pointers:
|
|
left: 4
|
|
mid: 5
|
|
right: 6
|
|
variables:
|
|
target: 9
|
|
left: 4
|
|
right: 6
|
|
mid: 5
|
|
codeHighlight:
|
|
startLine: 11
|
|
endLine: 12
|
|
|
|
- id: step-5
|
|
description: "Update right=4. Search space narrowed to index 4 only."
|
|
structures:
|
|
nums:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: visited }
|
|
- { value: 3, state: visited }
|
|
- { value: 5, state: visited }
|
|
- { value: 7, state: visited }
|
|
- { value: 9, state: active }
|
|
- { value: 11, state: visited }
|
|
- { value: 13, state: visited }
|
|
pointers:
|
|
left: 4
|
|
right: 4
|
|
variables:
|
|
target: 9
|
|
left: 4
|
|
right: 4
|
|
codeHighlight:
|
|
startLine: 4
|
|
endLine: 4
|
|
|
|
- id: step-6
|
|
description: "Calculate mid=4. nums[4]=9 == target=9. Found at index 4!"
|
|
structures:
|
|
nums:
|
|
type: array
|
|
values:
|
|
- { value: 1, state: visited }
|
|
- { value: 3, state: visited }
|
|
- { value: 5, state: visited }
|
|
- { value: 7, state: visited }
|
|
- { value: 9, state: found }
|
|
- { value: 11, state: visited }
|
|
- { value: 13, state: visited }
|
|
pointers:
|
|
mid: 4
|
|
variables:
|
|
target: 9
|
|
mid: 4
|
|
result: 4
|
|
codeHighlight:
|
|
startLine: 7
|
|
endLine: 8
|