Linear Search vs Binary Search: When Each Algorithm Wins
Compare linear and binary search algorithms, understand when to use each variant, and master binary search for production systems.
Linear search checks every element until it finds a match (O(n)), while binary search halves the search space each step on sorted data (O(log n)) — for a billion elements that is 30 comparisons instead of one billion. Binary search requires sorted data and random access, but the one-time cost of sorting pays off fast when you search repeatedly, and the variations (leftmost, rightmost, rotated arrays, search insert position) handle nearly every practical search scenario. You will know when linear search is the right default choice, how to implement binary search variants correctly without off-by-one errors, and how to apply these algorithms to production search problems like autocomplete and index range queries.
Linear Search vs Binary Search: Choosing the Right Algorithm
Search seems trivial until you’re doing it billions of times per second, or searching for the best solution among millions of possibilities. The difference between O(n) linear search and O(log n) binary search is the difference between finding a name in an unsorted phone book and a sorted one.
Binary search requires sorted data by the search key. This constraint is often worth satisfying, especially when you’ll search the same dataset multiple times.
Introduction
When you search a dataset only once on unsorted data, linear search is the obvious choice—sorting would cost more than the search itself. But if you search the same dataset repeatedly, the one-time cost of sorting (O(n log n)) pays off fast. For repeated searches on static data, building an index pays dividends.
Binary search uses the sorted structure to halve the search space each step, getting O(log n) instead of O(n). For a dataset of one billion elements, that’s 30 comparisons instead of one billion—a difference that directly impacts latency and compute costs in production systems.
These trade-offs—and binary search’s many flavors (leftmost, rightmost, on answers, in rotated arrays)—matter for writing code that scales.
Linear Search
def linear_search(arr, target):
"""
O(n) search - check each element sequentially.
Best for:
- Unsorted data
- Small datasets
- Single search on unsorted data
"""
for i, val in enumerate(arr):
if val == target:
return i
return -1
def linear_search_all(arr, target):
"""Find all occurrences of target - O(n)."""
return [i for i, val in enumerate(arr) if val == target]
def linear_search_with_sentinel(arr, target):
"""
Sentinel linear search - eliminates index check in loop.
One less comparison per iteration.
"""
n = len(arr)
last = arr[n - 1]
arr[n - 1] = target # Sentinel
i = 0
while arr[i] != target:
i += 1
arr[n - 1] = last # Restore
if i < n - 1 or arr[n - 1] == target:
return i
return -1
Binary Search (Iterative)
def binary_search(arr, target):
"""
O(log n) search in sorted array.
Requires:
- Array is sorted in ascending order
- Random access (array, not linked list)
"""
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2 # Prevents overflow
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
def binary_search_leftmost(arr, target):
"""
Find leftmost (first) occurrence of target.
Returns index where target should be inserted to maintain order.
"""
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
def binary_search_rightmost(arr, target):
"""
Find rightmost (last) occurrence of target.
Returns index after last occurrence.
"""
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 - 1
Binary Search Variations
def binary_search_rotated(arr, target):
"""
Search in rotated sorted array.
Array was [a, b, c, d, e, f] rotated to [d, e, f, a, b, c].
O(log n) time.
"""
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
return mid
# Left half is sorted
if arr[left] <= arr[mid]:
if arr[left] <= target < arr[mid]:
right = mid - 1
else:
left = mid + 1
# Right half is sorted
else:
if arr[mid] < target <= arr[right]:
left = mid + 1
else:
right = mid - 1
return -1
def find_minimum_rotated(arr):
"""Find minimum in rotated sorted array - O(log n)."""
left, right = 0, len(arr) - 1
while left < right:
mid = left + (right - left) // 2
if arr[mid] > arr[right]:
left = mid + 1
else:
right = mid
return left
def search_insert_position(arr, target):
"""
Find where target should be inserted.
Similar to leftmost binary search.
O(log n).
"""
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
def find_peak(arr):
"""
Find a peak element (not smaller than neighbors).
O(log n) for 1D array.
"""
left, right = 0, len(arr) - 1
while left < right:
mid = left + (right - left) // 2
if arr[mid] < arr[mid + 1]:
left = mid + 1
else:
right = mid
return left
Binary Search on Answer
def binary_search_on_answer(check_fn, low, high):
"""
Binary search when the predicate is monotonic.
Find minimum value that satisfies check_fn.
Example: Find minimum divisor such that division result <= threshold
"""
while low < high:
mid = low + (high - low) // 2
if check_fn(mid):
high = mid # Can do better, try lower
else:
low = mid + 1 # Need to increase
return low
def find_min_division(nums, threshold):
"""
Find minimum divisor such that sum of ceil(num/divisor) <= threshold.
"""
def can_divide(divisor):
total = sum((n + divisor - 1) // divisor for n in nums)
return total <= threshold
return binary_search_on_answer(can_divide, 1, max(nums))
def painters_partition problem(lengths, k, max_time):
"""
Find minimum time needed if k painters work in parallel.
"""
def can_finish(time):
painters_needed = 1
current_time = 0
for length in lengths:
if current_time + length <= time:
current_time += length
else:
painters_needed += 1
current_time = length
return painters_needed <= k
return binary_search_on_answer(can_finish, max(lengths), sum(lengths))
When to Use Each Search
| Scenario | Algorithm | Why |
|---|---|---|
| Unsorted data, one search | Linear | Sorting costs more than search |
| Sorted data, frequent search | Binary search | O(log n) vs O(n) per search |
| Sorted data, many searches | Sort + binary | Preprocessing pays off |
| Data stream, unknown size | Linear | Can’t binary search a stream |
| 2D sorted matrix | Binary search variants | Depends on structure |
Search Algorithm Decision Flow
graph TD
START["Input Array"] --> SORTED{"Sorted?"}
SORTED -->|"No"| LINEAR["Use Linear Search<br/>O(n), no preprocessing"]
SORTED -->|"Yes"| SIZE{"Array Size?"}
SIZE -->|"Small<br/>(< 100 elements)"| LINEAR2["Use Linear Search<br/>Overhead of binary not worth it"]
SIZE -->|"Large<br/>(>= 100 elements)"| BINARY["Use Binary Search<br/>O(log n)"]
BINARY --> ACCESS{"Random Access?"}
ACCESS -->|"No (linked list)"| LINEAR3["Use Linear Search<br/>No mid-point calculation possible"]
ACCESS -->|"Yes (array)"| BINARY2["Binary search works<br/>Calculate mid = left + (right-left)//2"]
BINARY2 --> HITS{"Number of Searches?"}
HITS -->|"Many (> sqrt(n))"| SORTFIRST["Sort first O(n log n), then Binary<br/>Total: O(n log n + k log n)"]
HITS -->|"Few (<= sqrt(n))"| BINARY3["Direct binary search O(k log n)<br/>Cheaper than sort cost"]
Time Complexity Comparison
| n | Linear O(n) | Binary O(log n) |
|---|---|---|
| 10 | 10 | 4 |
| 100 | 100 | 7 |
| 1,000 | 1,000 | 10 |
| 1,000,000 | 1,000,000 | 20 |
| 1,000,000,000 | 1,000,000,000 | 30 |
Trade-off Analysis
Linear Search vs Binary Search: Key Trade-offs
| Dimension | Linear Search | Binary Search |
|---|---|---|
| Time Complexity | O(n) | O(log n) |
| Space Complexity | O(1) iterative, no extra space | O(1) iterative, O(log n) recursive |
| Data Requirement | No preprocessing needed | Requires sorted data |
| Data Structure | Works on any iterable (arrays, lists, linked lists, streams) | Requires random access (arrays only) |
| Best Case | O(1) — target is first element | O(1) — target is middle element |
| Worst Case | O(n) — target is last or absent | O(log n) — target is absent or at edges |
| Insert Overhead | O(1) append at end | O(n) to maintain sorted order on insert |
| Cache Locality | Sequential access, cache-friendly | Random access pattern, cache misses on large arrays |
| Implementation Complexity | Trivial — single loop | Moderate — boundary conditions, off-by-one risks |
| Duplicates Handling | Returns first match; use linear_search_all for all | Needs leftmost/rightmost variants for comprehensive results |
| Streaming Data | Works natively | Not possible without known size |
| Small n (< 100) | Often faster due to lower overhead | Overhead of binary may not be worth it |
When the Sort-Once Cost Pays Off
| Scenario | Cost without sorting | Cost with sorting | Breakeven |
|---|---|---|---|
| Single search on N=10⁶ | 10⁶ comparisons (linear) | 10⁶ log₂(10⁶) ≈ 20·10⁶ (sort) + 20 (binary) | Not worth it |
| 100 searches on N=10⁶ | 100·10⁶ = 10⁸ (100M) | 20·10⁶ (sort) + 100·20 = ~20·10⁶ | Worth it after ~20 searches |
| 10⁶ searches on N=10³ | 10⁶·10³ = 10⁹ (1B) | 10³·log₂(10³) ≈ 10⁴ (sort) + 10⁶·10 = ~10⁷ | Worth it immediately |
Recursive vs Iterative Binary Search
| Aspect | Iterative Binary Search | Recursive Binary Search |
|---|---|---|
| Space Complexity | O(1) | O(log n) due to call stack |
| Risk | No stack overflow | Stack overflow for large arrays |
| Readability | Slightly more verbose | Cleaner, more self-documenting |
| Performance | No function call overhead per iteration | Function call overhead per recursion level |
| Use Case | Production systems, embedded systems | Interview settings, educational code |
Production Failure Scenarios and Mitigations
Four ways search implementations break in production—each with real-world consequences.
Integer overflow in binary search mid calculation
When calculating mid = (left + right) // 2, if left and right are very large integers (near INT_MAX), the addition can overflow in languages with fixed-size integers. This produces a negative mid value, causing unexpected behavior or infinite loops.
The overflow is real in C++, Java, and Go with 32-bit ints. Take left = 2_000_000_000 and right = 2_000_000_001 — individually they fit in a signed 32-bit int, but their sum 4_000_000_001 exceeds INT_MAX (2_147_483_647) and wraps negative. Accessing arr[negative_index] reads memory before the array, giving garbage or crashing. If the negative mid still passes left <= right, the loop never terminates — mid never advances past right.
Mitigation: Use mid = left + (right - left) // 2. The gap right - left never overflows because it’s always non-negative and at most the array length. In Rust, isize catches this in debug builds — but write safe code by default, not by relying on the runtime to save you. Validate input ranges before the search starts: reject or cap array sizes near your language’s integer limits.
# UNSAFE in C++/Java/Go — overflows with large indices
mid = (left + right) // 2
# SAFE — computes the gap first, then adds to left
mid = left + (right - left) // 2
Off-by-one errors causing infinite loops
Boundary condition errors in while loop termination cause left pointer to never advance past right, creating infinite loops. Common when handling edge cases like single-element arrays or when target is smaller/larger than all elements.
The bug hides in three specific places: wrong loop condition (left < right vs left <= right), wrong direction on mid update (mid + 1 when mid - 1 is correct), or initializing right to len(arr) instead of len(arr) - 1. A common trap: using while left < right with right = len(arr) gives a half-open range [left, right), but then mixing in closed-range logic — the last element never gets checked.
Mitigation: Write unit tests for empty array, single element (found and absent), target below all elements, target above all elements, and duplicates at boundaries. Pick one invariant and stick to it: closed-range (left <= right, right = len(arr) - 1) or open-range (left < right, right = len(arr)). This post uses closed-range. When left == right, exactly one element remains — the loop must process it before exiting.
Linear search on large unsorted arrays timing out
Linear search on an array with millions of elements with high latency per comparison (e.g., string comparison, complex object equality) causes request timeouts. Linear search is O(n) with a high constant factor that doesn’t show up in big-O notation.
Big-O notation papers over the slope. Ten million strings at 100 nanoseconds per comparison: roughly one second per search. That works in isolation. Under concurrent load, it falls apart. The data type matters as much as the algorithm — 10 million 64-bit integers at 100M comparisons/second takes 0.1 seconds; 10 million 50-character strings at 10M comparisons/second takes 1 second.
Mitigation: Sort before binary search when you’ll search multiple times. For strings, KMP or Boyer-Moore give O(n + m) with low constant factors. Set a hard timeout on linear scans — 100ms should cover about 1M cheap comparisons, not 1M string comparisons. If sorting isn’t worth it for a one-off search, build a hash index instead: O(n) insert, O(1) lookup, at the cost of O(n) extra memory.
Binary search on rotated array with ambiguous midpoint
In a rotated sorted array where the rotation point is near the middle, binary search decisions based on midpoint comparison can take the wrong branch, causing the search to miss the target element.
Standard binary search assumes one half is always sorted. Rotated arrays break that assumption. In [4, 5, 6, 1, 2, 3], you can determine which half is sorted by comparing arr[left] and arr[mid] — but when arr[left] == arr[mid], the comparison is useless. This happens with duplicate values near the rotation point: [1, 2, 2, 3, 4, 5, 6] rotated to [2, 3, 4, 5, 6, 1, 2] puts the pivot near the middle and leaves you unable to tell which side is sorted.
Concrete failure: search for 2 in [2, 2, 2, 2, 2, 2] (uniform array, effectively not rotated). Every arr[mid] == arr[left], so the branch condition never switches sides, and the search halves incorrectly. When arr[left] == arr[mid] == arr[right], fall back to linear search — don’t guess.
Mitigation: When mid is in the “unordered” region (array[mid] < array[left] but target > array[mid]), explicitly verify which side is sorted before committing to a branch. Verify the chosen branch actually contains the target before recursing. Add a duplicate-handling fallback: if arr[left] == arr[mid] == arr[right], shrink both ends (left++, right--) rather than trying to branch on an inconclusive comparison.
Quick Recap Checklist
- Linear search: O(n), works on unsorted data
- Binary search: O(log n), requires sorted data + random access
- Use leftmost/rightmost variants for counting occurrences
- Binary search on answer for optimization problems
- Watch for integer overflow when calculating mid
- In rotated arrays, one half is always sorted
Observability Checklist
Track these signals to catch search degradation before it becomes an incident:
Core Metrics
- Search operation latency (p50, p95, p99) per algorithm type
- Array size at search time
- Number of comparisons per search
- Binary search iteration count (should stay around O(log n))
- Cache hit/miss rate during search operations
Health Signals
- Comparison count exceeding expected O(log n) by 2x or more — something’s off with boundaries
- Binary search taking more than 60 iterations for arrays under 1 billion elements
- Latency p99 > 10ms for single search operation
- Linear search on arrays larger than 10K elements occurring frequently
Alerting Thresholds
- Binary search iteration count > 40 for any array: investigate overflow or boundary bug
- Linear search on array > 100K elements: consider sorting first
- Latency p99 > 100ms for any single search: alert immediately
- Error rate on search operations > 1%: alert
Distributed Tracing
- Trace search operations end-to-end
- Include array size, algorithm type, and iteration count as span attributes
- Correlate slow searches with large array operations or GC pauses
Security Notes
Search algorithms have specific timing and information risks.
Timing side channels revealing search behavior
The time taken by a search operation can leak information about the data being searched. Linear search on sensitive data where early termination depends on key value reveals whether the key was found early or late in the array.
Linear search exits on first match. Measure the response time: fast means near the start, slow means near the end. In a username lookup table where the return value is a bcrypt hash, this reveals whether an account exists — useful for targeted attacks. Microsecond differences are detectable with enough samples.
Mitigation: Use constant-time comparison for sensitive data. Always scan the full array (or all hash buckets) and return a dummy on miss, so every lookup takes the same time. For binary search, the branching pattern itself is a timing signal — evaluate both branches and select the correct result using a constant-time conditional move instead of branching on arr[mid] < target.
Binary search timing as a power analysis vector
In cryptographic implementations, the branching in binary search (comparing array[mid] to target) can leak information through cache timing. An attacker with access to timing measurements could infer key values.
Binary search branches on arr[mid] < target, taking different paths depending on data values. Those paths access different memory regions, producing measurably different cache timing profiles. In a cryptographic key table search, an attacker with access to power or cache timing measurements can infer which branch was taken at each step, reconstructing the key bit by bit.
Mitigation: Use constant-time binary search — evaluate both branches on every iteration, then select the correct next state with a constant-time conditional move. This keeps memory access patterns data-independent. Cryptographic libraries like libsodium provide these primitives because getting it right from scratch is harder than it looks.
Linear search on adversarial input causing CPU exhaustion
An attacker who can control input data can cause a service to perform linear searches on very large unsorted arrays, consuming CPU and causing denial of service.
Pass 50 million unsorted strings to a linear-search endpoint — at 1 microsecond per comparison, that’s 50 seconds of CPU per request. A few concurrent requests tie up the thread pool. The surface includes any API that accepts a dataset and searches it: import functions, filter endpoints, batch lookup services.
Real example: a file sharing service that checks for duplicate tags by scanning an unsorted list linearly. An attacker with 10 million tags forces every subsequent lookup to scan all 10 million. At 1 microsecond per comparison, one search takes 10 seconds. At 100 requests per second, that’s 1000 seconds of CPU per second — a 10x overload on a 100-core machine.
Mitigation: Set maximum array size limits on linear search operations and reject anything above them. Use a hash set for tag deduplication — O(1) insert and lookup, bounded and predictable time cost. Add per-request CPU time budgets at the application layer, and fail fast (return an error or empty result) rather than letting the thread pool saturate.
Interview Questions
To prevent integer overflow. In languages with fixed-size
integers (C++, Java), adding two large values can overflow. For example,
(2^31 - 1) + (2^31 - 1) overflows the 32-bit signed integer.
Using low + (high - low) // 2 computes the difference first,
which stays within bounds. Python has arbitrary precision integers, so this
isn't necessary, but it's a best practice for portable code.
Start from top-right corner (or bottom-left). The key insight: if current element > target, move left; if current element < target, move down. This is O(m + n) worst case, leveraging the sorted properties of both rows and columns. Alternatively, binary search on rows then binary search on columns for O(log m + log n) but more complex to implement.
Lower bound returns the first position where target could be inserted (first occurrence for existing values). Upper bound returns the position after the last occurrence. If target exists at positions 3, 4, 5, lower_bound gives 3, upper_bound gives 6. The number of occurrences = upper_bound - lower_bound. Both are O(log n) via binary search variants.
When you need to find an optimal value (minimum or maximum) and you can check if a candidate is feasible, binary search on the answer finds the optimum. If checking a value V is feasible and V+1 is also feasible, the predicate is monotonic—binary search applies. Example: "find minimum time to complete K painters" — if you can finish in time T, you can finish in any time > T. Check(T) is monotonic.
Recursive binary search:
def binary_search_recursive(arr, target, left, right):
if left > right:
return -1
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)
Trade-offs: Recursive version is cleaner and more readable but uses O(log n) stack space vs O(1) for iterative. Risk of stack overflow for very large arrays. Iterative is preferred in production; recursive is common in interviews.
Use exponential search (also called galloping search):
- Start with a range of size 1, double it until the range end exceeds the target or you hit the end of the array
- Then perform binary search within the identified range
- Overall complexity: O(log i) where i is the index of the target
- If the array is truly infinite (no end), read with try/except to handle out-of-bounds
Binary search on the answer space [0, x] (or [0, x/2] for x >= 4):
def sqrt_binary_search(x, precision=1e-6):
low, high = 0, max(1, x)
while high - low > precision:
mid = (low + high) / 2
if mid * mid < x:
low = mid
else:
high = mid
return (low + high) / 2
The predicate mid * mid >= x is monotonic, making binary search applicable. For integer square root, use mid * mid <= x as the check and return low.
A peak element is an element that is not smaller than its neighbors. Binary search works because the array implicitly has a peak somewhere:
- Pick the middle element; if it's a peak, return it
- If
arr[mid] < arr[mid + 1], then a peak exists in the right half (the array is ascending at mid, so it must eventually descend) - If
arr[mid] < arr[mid - 1], then a peak exists in the left half - Time complexity: O(log n) for 1D arrays
Time complexity:
- Best case: O(1) — target is the first element
- Average case: O(n) — assuming uniform distribution, need to check n/2 elements
- Worst case: O(n) — target is the last element or absent
Space complexity: O(1) — iterative, no extra space beyond a few variables. The sentinel variant also uses O(1) space.
Use leftmost + rightmost binary search:
- Find the first occurrence (lower_bound / leftmost binary search)
- Find the last occurrence (upper_bound - 1 / rightmost binary search)
- Frequency = (last_index - first_index + 1) if the element exists, else 0
- Time complexity: O(log n) — two binary searches, each O(log n)
Sentinel linear search eliminates the index-out-of-bounds check from the loop body:
- Place a sentinel (the target value) at the end of the array, saving the original last element
- The loop runs
while arr[i] != targetwithout checkingi < n - After the loop, restore the last element and check whether the target was found at a valid position
- This reduces comparisons per iteration from 2 (bounds check + equality) to 1 (equality only)
- Useful when comparisons are expensive and n is large
Search with a modified binary search that determines which half is sorted:
- Compare
arr[mid]witharr[left]to determine if the left half is sorted - If left half is sorted and target lies within it, search left; otherwise search right
- If left half is not sorted, then the right half must be sorted — check if target lies there
- Time complexity: O(log n)
Edge cases: Single-element arrays, array rotated by 0 positions (no rotation), array rotated by n/2 (pivot at middle), duplicates in rotated arrays (requires linear fallback), target equal to arr[left] or arr[right].
Unlike other O(n) algorithms that process or transform data, linear search is a query operation that simply checks equality. Key distinctions:
- Works on any iterable with traversal access — arrays, linked lists, streams, generators
- Short-circuits on first match — best case O(1)
- No preprocessing required — no sorting, no index building
- Handles unsorted data natively — no assumptions about data organization
- This is the core tension in search: you can't beat O(n) for unordered single searches without domain knowledge, but once data is sorted, you can get O(log n).
Use a binary search on the value range combined with count operations:
- The matrix has min = matrix[0][0] and max = matrix[n-1][m-1]
- Binary search on [min, max] for the k-th smallest value
- For each mid, count how many elements are <= mid (walk from top-right to bottom-left)
- If count >= k, move high = mid; else low = mid + 1
- Time complexity: O(log(max-min) * (n + m)) — typically O(32 * (n+m)) for 32-bit integers
Alternative: Min-heap approach O(k log n) but binary search is better for small k values in large matrices.
Binary search and BSTs share the same divide-and-conquer philosophy but differ in structure and use cases:
- Binary search operates on sorted arrays with O(1) random access — O(log n) search via index arithmetic
- BST maintains sorted order dynamically — O(log n) average search via pointer following
- BST supports insert/delete in O(log n); array binary search requires O(n) to maintain sorted order
- BST has worse cache locality due to pointer chasing; array binary search is cache-friendly
- Both achieve O(log n) on balanced structures; degrade to O(n) if unbalanced (BST) or unusable if unsorted (binary search)
Binary search's time complexity O(log n) is distribution-independent, but practical performance varies:
- Uniform distribution: Midpoint is representative; constant-time per iteration
- Clumped distribution: Logarithmic iterations unchanged, but comparisons may be faster (early termination more likely)
- Sorted with frequent small values: Left-heavy splits increase iterations slightly; boundary conditions matter more
- Hot data in cache: Array binary search benefits from sequential mid access pattern
- Distribution affects branch prediction — sorted ascending data may mispredict more often on the else branch
Ternary search finds extrema in unimodal functions but is rarely the right choice:
- Ternary divides into 3 parts (2 mids), requiring 4 comparisons per iteration vs binary's 1-2
- Time complexity: O(log₃ n) ≈ O(1.58 * log₂ n) — slower than binary's O(log₂ n)
- Use cases: Finding peaks in 2D arrays, ternary search on continuous unimodal functions
- Binary search on the derivative is often preferable for 1D optimization
- In practice: Always prefer binary search for discrete search spaces; ternary only when explicitly asked or for specialized 2D peak finding
Binary search on floating point requires epsilon-based termination instead of exact equality:
def binary_search_float(arr, target, eps=1e-9): low, high = 0, len(arr) - 1 while high - low > eps: mid = (low + high) / 2 if arr[mid] < target: low = mid else: high = mid return (low + high) / 2
def binary_search_float_relative(arr, target, rel_eps=1e-6): low, high = arr[0], arr[-1] while high - low > rel_eps * max(abs(low), abs(high)): mid = (low + high) / 2 if mid < target: low = mid else: high = mid return mid
Key considerations: Use relative epsilon for large values, absolute epsilon for precision requirements, account for floating point rounding errors in comparisons.
An adaptive search system analyzes data characteristics at runtime:
def adaptive_search(arr, target):
n = len(arr)
if n < 64:
return linear_search(arr, target) # Overhead not worth it
if not is_sorted(arr):
# Check if sorting would pay off
if n * 10 > estimated_future_searches() * math.log2(n):
return linear_search(arr, target)
arr = arr.sort() # Now we can binary search
return binary_search(arr, target)
- Threshold-based: Small arrays (n < 100) use linear; overhead not justified
- Sorting decision: Sort if expected searches > n/log n
- Data structure check: Detect sortedness in O(n) before committing to binary search
- Monitoring: Track search patterns and adapt periodically
Binary search halves the search space each iteration. For an array of size n:
- After iteration 1: at most n/2 elements remain
- After iteration 2: at most n/4 elements remain
- After iteration k: at most n/2^k elements remain
- Search ends when n/2^k < 1, i.e., 2^k > n
- Solving: k > log₂ n, so k = ⌈log₂ n⌉
- Since each iteration does O(1) work, total time = O(log n)
Proof by induction also confirms correctness: at each step, the target must be in the half containing it, and we recursively search that half until found or exhausted.
Further Reading
-
Binary Search Variants — Deep dive into leftmost, rightmost, and rotated binary search variants with production-ready code
-
Big O Notation Guide — Visual explanation of asymptotic complexity with real-world analogies
-
Array Sorting Algorithms — How sorting enables binary search and trade-offs between sorting algorithms
-
Binary Search on Answer: A Complete Guide — Monotonic predicates and optimization problems solved via binary search
-
Introduction to Algorithms (CLRS) — Chapter 2 covers searching fundamentals; Chapter 12 covers binary search trees
Conclusion
Binary search is deceptively simple but easy to get wrong at the edges. Remember: always use low + (high - low) // 2 to avoid overflow, and know which variant you need (leftmost vs rightmost vs plain match). When you don’t have sorted data but need to search, linear search is your only option—and sentinel search is a minor optimization worth knowing. For monotonic predicates in optimization problems, binary search on the answer is often the key to getting O(log n) instead of O(n). For more on algorithm patterns, see Binary Search Variants.
Category
Related Posts
Binary Search Variants: Beyond Simple Lookup
Master variations of binary search including lower bound, upper bound, search in rotated array, and fractional searching for optimization problems.
Divide and Conquer: Breaking Problems into Subproblems
Master the divide and conquer paradigm with classic examples like merge sort, quicksort, and binary search.
Space Complexity Analysis: Measuring and Optimizing Memory
Master space complexity analysis — learn how to measure, analyze, and optimize the memory footprint of algorithms from O(1) to O(n²) and beyond.