Binary Search
Binary search is an efficient algorithm for finding a target value in a sorted array by repeatedly dividing the search interval in half.
Basic Algorithm
Concept
Compare the target with the middle element:
- If equal: found
- If target < middle: search left half
- If target > middle: search right half
Mathematical Formulation
For a sorted array $A[0 \ldots n-1]$ and target $x$:
$$ \text{BinarySearch}(A, x, \text{left}, \text{right}) = \begin{cases} -1 & \text{if left} > \text{right} \\ \text{mid} & \text{if } A[\text{mid}] = x \\ \text{BinarySearch}(A, x, \text{left}, \text{mid}-1) & \text{if } A[\text{mid}] > x \\ \text{BinarySearch}(A, x, \text{mid}+1, \text{right}) & \text{if } A[\text{mid}] < x \end{cases} $$
where: $$ \text{mid} = \text{left} + \lfloor (\text{right} - \text{left}) / 2 \rfloor $$
Note: Use left + (right - left) / 2 instead of (left + right) / 2 to avoid integer overflow.
Go Implementation: Basic Binary Search
1// BinarySearch finds target in sorted array, returns index or -1
2func BinarySearch(arr []int, target int) int {
3 left, right := 0, len(arr)-1
4
5 for left <= right {
6 // Avoid overflow: left + (right-left)/2
7 mid := left + (right-left)/2
8
9 if arr[mid] == target {
10 return mid // Found
11 } else if arr[mid] < target {
12 left = mid + 1 // Search right half
13 } else {
14 right = mid - 1 // Search left half
15 }
16 }
17
18 return -1 // Not found
19}
20
21// Example usage
22func main() {
23 arr := []int{1, 3, 5, 7, 9, 11, 13, 15}
24 fmt.Println(BinarySearch(arr, 7)) // Output: 3
25 fmt.Println(BinarySearch(arr, 6)) // Output: -1
26}
Recursive Implementation
1func BinarySearchRecursive(arr []int, target, left, right int) int {
2 if left > right {
3 return -1
4 }
5
6 mid := left + (right-left)/2
7
8 if arr[mid] == target {
9 return mid
10 } else if arr[mid] < target {
11 return BinarySearchRecursive(arr, target, mid+1, right)
12 } else {
13 return BinarySearchRecursive(arr, target, left, mid-1)
14 }
15}
Complexity Analysis
Time Complexity
Recurrence relation: $$ T(n) = T(n/2) + O(1) $$
Solution (by Master Theorem): $$ T(n) = O(\log n) $$
Proof by iteration count: After $k$ iterations, search space is $n / 2^k$. When search space becomes 1: $$ \frac{n}{2^k} = 1 \implies k = \log_2 n $$
Space Complexity
- Iterative: $O(1)$
- Recursive: $O(\log n)$ due to call stack
Implementation Variants
1. Find Exact Match
Returns index if found, -1 otherwise.
Invariant: If target exists, it's in [left, right]
2. Find First Occurrence (Lower Bound)
Find the leftmost position where target appears or could be inserted.
Invariant: A[left-1] < target and A[right+1] >= target
1// LowerBound finds first position where arr[i] >= target
2// Returns insertion point if target not found
3func LowerBound(arr []int, target int) int {
4 left, right := 0, len(arr)
5
6 for left < right {
7 mid := left + (right-left)/2
8
9 if arr[mid] < target {
10 left = mid + 1
11 } else {
12 right = mid
13 }
14 }
15
16 return left
17}
18
19// Example
20func main() {
21 arr := []int{1, 2, 2, 2, 3, 4, 5}
22 fmt.Println(LowerBound(arr, 2)) // Output: 1 (first occurrence)
23 fmt.Println(LowerBound(arr, 6)) // Output: 7 (insertion point)
24}
Result:
- If
left < nandA[left] == target: first occurrence atleft - Otherwise: insertion point is
left
3. Find Last Occurrence (Upper Bound)
Find the rightmost position where target appears.
1// UpperBound finds first position where arr[i] > target
2func UpperBound(arr []int, target int) int {
3 left, right := 0, len(arr)
4
5 for left < right {
6 mid := left + (right-left)/2
7
8 if arr[mid] <= target {
9 left = mid + 1
10 } else {
11 right = mid
12 }
13 }
14
15 return left
16}
17
18// FindLastOccurrence returns index of last occurrence or -1
19func FindLastOccurrence(arr []int, target int) int {
20 pos := UpperBound(arr, target) - 1
21 if pos >= 0 && pos < len(arr) && arr[pos] == target {
22 return pos
23 }
24 return -1
25}
26
27// Example
28func main() {
29 arr := []int{1, 2, 2, 2, 3, 4, 5}
30 fmt.Println(FindLastOccurrence(arr, 2)) // Output: 3 (last occurrence)
31}
4. Count Occurrences
1// CountOccurrences counts how many times target appears
2func CountOccurrences(arr []int, target int) int {
3 first := LowerBound(arr, target)
4 last := UpperBound(arr, target)
5
6 if first < len(arr) && arr[first] == target {
7 return last - first
8 }
9 return 0
10}
11
12// Example
13func main() {
14 arr := []int{1, 2, 2, 2, 3, 4, 5}
15 fmt.Println(CountOccurrences(arr, 2)) // Output: 3
16}
Advanced Variations
1. Search in Rotated Sorted Array
Array is sorted but rotated at some pivot.
Example: [4, 5, 6, 7, 0, 1, 2]
1func SearchRotated(arr []int, target int) int {
2 left, right := 0, len(arr)-1
3
4 for left <= right {
5 mid := left + (right-left)/2
6
7 if arr[mid] == target {
8 return mid
9 }
10
11 // Determine which half is sorted
12 if arr[left] <= arr[mid] {
13 // Left half is sorted
14 if arr[left] <= target && target < arr[mid] {
15 right = mid - 1 // Target in left half
16 } else {
17 left = mid + 1 // Target in right half
18 }
19 } else {
20 // Right half is sorted
21 if arr[mid] < target && target <= arr[right] {
22 left = mid + 1 // Target in right half
23 } else {
24 right = mid - 1 // Target in left half
25 }
26 }
27 }
28
29 return -1
30}
31
32// Example
33func main() {
34 arr := []int{4, 5, 6, 7, 0, 1, 2}
35 fmt.Println(SearchRotated(arr, 0)) // Output: 4
36}
Time: $O(\log n)$
2. Find Minimum in Rotated Array
1func FindMin(arr []int) int {
2 left, right := 0, len(arr)-1
3
4 for left < right {
5 mid := left + (right-left)/2
6
7 if arr[mid] > arr[right] {
8 // Minimum is in right half
9 left = mid + 1
10 } else {
11 // Minimum is in left half (including mid)
12 right = mid
13 }
14 }
15
16 return arr[left]
17}
18
19// Example
20func main() {
21 arr := []int{4, 5, 6, 7, 0, 1, 2}
22 fmt.Println(FindMin(arr)) // Output: 0
23}
Time: $O(\log n)$
3. Find Peak Element
Element greater than its neighbors.
1func FindPeakElement(arr []int) int {
2 left, right := 0, len(arr)-1
3
4 for left < right {
5 mid := left + (right-left)/2
6
7 if arr[mid] < arr[mid+1] {
8 // Peak is to the right
9 left = mid + 1
10 } else {
11 // Peak is to the left (including mid)
12 right = mid
13 }
14 }
15
16 return left
17}
18
19// Example
20func main() {
21 arr := []int{1, 2, 3, 1}
22 fmt.Println(FindPeakElement(arr)) // Output: 2 (value 3)
23
24 arr2 := []int{1, 2, 1, 3, 5, 6, 4}
25 fmt.Println(FindPeakElement(arr2)) // Output: 5 (value 6)
26}
Time: $O(\log n)$
4. Search in 2D Matrix
1// Search2DMatrix searches in row-sorted and column-sorted matrix
2func Search2DMatrix(matrix [][]int, target int) bool {
3 if len(matrix) == 0 || len(matrix[0]) == 0 {
4 return false
5 }
6
7 m, n := len(matrix), len(matrix[0])
8 left, right := 0, m*n-1
9
10 for left <= right {
11 mid := left + (right-left)/2
12 // Convert 1D index to 2D coordinates
13 row, col := mid/n, mid%n
14 midVal := matrix[row][col]
15
16 if midVal == target {
17 return true
18 } else if midVal < target {
19 left = mid + 1
20 } else {
21 right = mid - 1
22 }
23 }
24
25 return false
26}
27
28// Example
29func main() {
30 matrix := [][]int{
31 {1, 3, 5, 7},
32 {10, 11, 16, 20},
33 {23, 30, 34, 60},
34 }
35 fmt.Println(Search2DMatrix(matrix, 3)) // Output: true
36 fmt.Println(Search2DMatrix(matrix, 13)) // Output: false
37}
Time: $O(\log(m \times n))$
Binary Search on Answer
When the answer space is monotonic, binary search can find optimal value.
Pattern
- Define search space
[left, right] - Define feasibility function
canAchieve(mid) - Binary search on the answer
Example: Capacity To Ship Packages Within D Days
Problem: Find minimum ship capacity to ship all packages in D days.
1func ShipWithinDays(weights []int, days int) int {
2 // Search space: [max(weights), sum(weights)]
3 left, right := 0, 0
4 for _, w := range weights {
5 if w > left {
6 left = w
7 }
8 right += w
9 }
10
11 // Binary search on capacity
12 for left < right {
13 mid := left + (right-left)/2
14
15 if canShip(weights, days, mid) {
16 right = mid // Try smaller capacity
17 } else {
18 left = mid + 1 // Need larger capacity
19 }
20 }
21
22 return left
23}
24
25// canShip checks if we can ship with given capacity in days
26func canShip(weights []int, days, capacity int) bool {
27 daysNeeded, currentLoad := 1, 0
28
29 for _, w := range weights {
30 if currentLoad+w > capacity {
31 daysNeeded++
32 currentLoad = w
33 if daysNeeded > days {
34 return false
35 }
36 } else {
37 currentLoad += w
38 }
39 }
40
41 return true
42}
43
44// Example
45func main() {
46 weights := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
47 days := 5
48 fmt.Println(ShipWithinDays(weights, days)) // Output: 15
49}
Time: $O(n \log(\text{sum} - \text{max}))$
Example: Koko Eating Bananas
1func MinEatingSpeed(piles []int, h int) int {
2 left, right := 1, 0
3 for _, p := range piles {
4 if p > right {
5 right = p
6 }
7 }
8
9 for left < right {
10 mid := left + (right-left)/2
11
12 if canFinish(piles, h, mid) {
13 right = mid // Try slower speed
14 } else {
15 left = mid + 1 // Need faster speed
16 }
17 }
18
19 return left
20}
21
22func canFinish(piles []int, h, speed int) bool {
23 hours := 0
24 for _, p := range piles {
25 hours += (p + speed - 1) / speed // Ceiling division
26 }
27 return hours <= h
28}
29
30// Example
31func main() {
32 piles := []int{3, 6, 7, 11}
33 h := 8
34 fmt.Println(MinEatingSpeed(piles, h)) // Output: 4
35}
Time: $O(n \log(\text{max}))$
Mathematical Properties
Number of Comparisons
Best case: 1 (target is at middle)
Worst case: $\lceil \log_2(n+1) \rceil$
Average case: $\log_2 n - 1$
Decision Tree Height
Binary search corresponds to a decision tree of height: $$ h = \lceil \log_2(n+1) \rceil $$
This is optimal for comparison-based search (information-theoretic lower bound).
Search Space Reduction
After $k$ iterations: $$ \text{remaining elements} = \frac{n}{2^k} $$
Common Pitfalls
1. Integer Overflow
Wrong:
1mid := (left + right) / 2 // Can overflow!
Correct:
1mid := left + (right-left)/2 // Safe from overflow
2. Infinite Loop
Problem: mid calculation can cause infinite loop.
Wrong:
1// When left = 1, right = 2, mid = 1 forever
2for left < right {
3 mid := (left + right) / 2
4 if arr[mid] <= target {
5 left = mid // Infinite loop!
6 }
7}
Correct:
1for left < right {
2 mid := left + (right-left+1)/2 // Rounds up
3 if arr[mid] <= target {
4 left = mid
5 } else {
6 right = mid - 1
7 }
8}
3. Off-by-One Errors
Common mistakes:
- Using
left <= rightvs.left < right - Updating with
midvs.mid ± 1 - Return value off by one
Solution: Maintain clear invariants and test boundary cases.
Comparison with Other Search Algorithms
| Algorithm | Time (Average) | Time (Worst) | Space | Requirement |
|---|---|---|---|---|
| Binary Search | $O(\log n)$ | $O(\log n)$ | $O(1)$ | Sorted array |
| Linear Search | $O(n)$ | $O(n)$ | $O(1)$ | None |
| Jump Search | $O(\sqrt{n})$ | $O(\sqrt{n})$ | $O(1)$ | Sorted array |
| Interpolation Search | $O(\log \log n)$ | $O(n)$ | $O(1)$ | Uniformly distributed |
| Exponential Search | $O(\log n)$ | $O(\log n)$ | $O(1)$ | Unbounded sorted array |
Applications
- Dictionary lookup: Fast word search
- Database indexing: B-tree search
- Version control: Git bisect for finding bugs
- Debugging: Finding first failing test
- Game development: AI decision trees
- Numerical methods: Root finding, optimization
- Computer graphics: Ray tracing, collision detection
When to Use Binary Search
✅ Use when:
- Data is sorted (or has monotonic property)
- Need $O(\log n)$ search time
- Random access is available (arrays)
- Search space is large
❌ Don't use when:
- Data is unsorted and sorting cost > search benefit
- Data structure doesn't support random access (linked lists)
- Search space is small (linear search is simpler)
- Need to find all occurrences (may need linear scan anyway)
Related Snippets
- Binary Tree
A binary tree is a tree data structure where each node has at most two children, … - General Tree
A tree is a hierarchical data structure consisting of nodes connected by edges, … - Heap Data Structure
A heap is a specialized tree-based data structure that satisfies the heap … - Heap Sort
Heap sort is a comparison-based sorting algorithm that uses a binary heap data … - Linked List Operations
Optimal techniques for common linked list operations using pointer manipulation, … - LRU and LFU Cache
Cache replacement policies determine which items to evict when the cache is … - Merge Sort
Merge sort is a stable, comparison-based sorting algorithm that uses the … - Quadtree
A Quadtree is a tree data structure where each internal node has exactly four … - Quick Sort
Quick sort is an efficient, in-place, comparison-based sorting algorithm that … - String Similarity Algorithms
Algorithms for measuring similarity between strings, used in spell checking, DNA … - Terrain & Heightmap Generation
Procedural terrain generation creates realistic landscapes using mathematical … - Trie (Prefix Tree)
A Trie (pronounced "try") is a tree-like data structure used to store … - Wave Function Collapse
Wave Function Collapse (WFC) is a procedural generation algorithm that creates …