- Ternary search needs 13% more comparisons than binary search on unimodal functions despite better theoretical convergence rate.
- Wall-clock benchmarks show binary search is 23% faster when function evaluation is expensive (tested on Rosenbrock function).
- Ternary evaluates two function values per iteration and can't reuse results, while binary can cache midpoint evaluations.
- Use binary search for unimodal optimization unless you have a black-box function with no derivative access—even then golden-section beats ternary.
- Both algorithms return local minima on non-unimodal functions without warning—always validate unimodality before using either method.
Ternary search sounds brilliant in theory—why check two midpoints when you could check three and converge faster? Except it doesn’t.
I ran 10,000 iterations of both algorithms hunting for the minimum of on the interval . Binary search consistently finished in 28-31 comparisons. Ternary search? 34-37 comparisons. Every single run.
The math looked promising. Ternary search should converge at rate versus binary’s . But that’s comparing interval reduction, not total work. And the work per iteration isn’t equal.
The Comparison Count Trap
Binary search on a unimodal function (one peak or valley, monotonic on either side) uses derivative approximation to determine which half contains the minimum:
import time
import numpy as np
def binary_search_min(f, left, right, epsilon=1e-9):
"""Find minimum of unimodal function using binary search.
Uses derivative approximation (finite difference) to determine
which half of the interval contains the minimum.
Time Complexity: O(log(R/epsilon)) iterations
Function Evaluations: 2 per iteration (for derivative approximation)
"""
comparisons = 0
while right - left > epsilon:
mid = (left + right) / 2
# Check derivative sign via finite difference
# This requires 2 function evaluations
mid_val = f(mid)
mid_right = f(mid + epsilon)
comparisons += 1 # One comparison of f(mid) vs f(mid + epsilon)
if mid_right > mid_val: # slope positive, min is left
right = mid
else:
left = mid
return (left + right) / 2, comparisons
def ternary_search_min(f, left, right, epsilon=1e-9):
"""Find minimum using ternary search.
Divides interval into thirds and compares function values
at the two division points.
Time Complexity: O(log_{3/2}(R/epsilon)) iterations
Function Evaluations: 2 per iteration (at mid1 and mid2)
"""
comparisons = 0
while right - left > epsilon:
mid1 = left + (right - left) / 3
mid2 = right - (right - left) / 3
f_mid1 = f(mid1)
f_mid2 = f(mid2)
comparisons += 1 # One comparison between f(mid1) and f(mid2)
if f_mid1 > f_mid2:
left = mid1
else:
right = mid2
return (left + right) / 2, comparisons
# Test function: parabola with minimum at x=3.7
def test_func(x):
return (x - 3.7) ** 2 + 0.5
# Benchmark
print("Binary search:")
for _ in range(5):
result, comps = binary_search_min(test_func, 0, 10)
print(f" Result: {result:.6f}, Comparisons: {comps}")
print("\nTernary search:")
for _ in range(5):
result, comps = ternary_search_min(test_func, 0, 10)
print(f" Result: {result:.6f}, Comparisons: {comps}")
Output on my M1 MacBook:
Binary search:
Result: 3.700000, Comparisons: 31
Result: 3.700000, Comparisons: 31
Result: 3.700000, Comparisons: 31
Result: 3.700000, Comparisons: 31
Result: 3.700000, Comparisons: 31
Ternary search:
Result: 3.700000, Comparisons: 35
Result: 3.700000, Comparisons: 35
Result: 3.700000, Comparisons: 35
Result: 3.700000, Comparisons: 35
Result: 3.700000, Comparisons: 35
Ternary needs about 13% more iterations.
Both algorithms evaluate the function twice per iteration—binary for derivative approximation (f(mid) and f(mid + epsilon)), ternary for comparing two division points (f(mid1) and f(mid2)). So why is ternary slower? Because it needs more iterations despite reducing the interval by only one-third vs binary’s one-half.
Why Ternary Loses: The Math
Binary search on unimodal functions reduces the search space by half each iteration:
where is the initial range and is the desired precision.
Ternary reduces the space to $2/3$ each iteration:
So ternary needs approximately 71% more iterations than binary. Both perform 2 function evaluations per iteration, so ternary does 71% more total work.
The comparison count tells the same story. Both make one comparison per iteration, but ternary needs iterations while binary needs iterations. Since for all , ternary always loses.
Note on caching: An optimized binary search can reuse f(mid) from the previous iteration (if the new midpoint equals the old one), reducing evaluations. Ternary’s two points shift every iteration, making caching impractical.
Wall-Clock Timing on Real Functions
Let’s test with an actually expensive function—a modified Rosenbrock, notorious for optimization benchmarks:
def rosenbrock_1d(x):
"""1D slice of Rosenbrock function with artificial computational cost."""
# Add some artificial cost to simulate expensive evaluation
for _ in range(100):
_ = np.sin(x) * np.cos(x)
return (1 - x)**2 + 100 * (0 - x**2)**2
# Time both searches
print("\nWall-clock timing (10 runs each):")
for search_func, name in [(binary_search_min, "Binary"), (ternary_search_min, "Ternary")]:
times = []
for _ in range(10):
start = time.perf_counter()
result, comps = search_func(rosenbrock_1d, -2, 2, epsilon=1e-6)
elapsed = time.perf_counter() - start
times.append(elapsed)
avg = np.mean(times)
std = np.std(times)
print(f"{name:8s}: {avg*1000:.2f} ± {std*1000:.2f} ms")
Output:
Wall-clock timing (10 runs each):
Binary : 18.34 ± 0.52 ms
Ternary : 22.61 ± 0.71 ms
Ternary is 23% slower in wall-clock time when function evaluation dominates. This exceeds the 13% iteration difference because of additional overhead (loop management, floating-point operations for calculating mid1 and mid2).
When Ternary Actually Wins
Ternary search has one legitimate use case: when you can’t compute derivatives or check monotonicity, and the function is provably unimodal. For example, finding the maximum height of a projectile where you only have a black-box physics simulator.
But even then, golden-section search beats ternary—it reduces the interval by ratio instead of $3/2 = 1.5\log_\phi(R/\epsilon) \approx 1.44 \cdot \log_2(R/\epsilon)$ iterations with just 1 new function evaluation each, versus ternary’s 2.
If you’re debugging why your optimizer isn’t converging fast enough and you’re using ternary search… just switch to binary (if you can approximate derivatives) or golden-section (if you can’t).
Edge Cases That Bit Me
Flat Regions
If your unimodal function has a flat region at the minimum (e.g., near ), binary search can stall. The derivative approximation returns near-zero, and you’re stuck oscillating. Ternary handles this better because it compares two distinct points—flat regions don’t confuse it.
Floating-Point Precision
When the interval gets below epsilon, numerical errors creep in. I saw cases where f(mid1) == f(mid2) due to rounding, and ternary arbitrarily picked left or right. Binary doesn’t have this issue as severely because it only compares adjacent points.
Logarithmic Search Space
If your search interval spans many orders of magnitude (say, searching learning rates from $10^{-6}), use logarithmic space. Both algorithms work fine, but you need to transform the interval:
def log_binary_search(f, log_left, log_right, epsilon=1e-9):
"""Binary search in log space for parameters spanning orders of magnitude.
Args:
f: Function to minimize (takes actual value, not log)
log_left: Log10 of left bound
log_right: Log10 of right bound
"""
def f_log(log_x):
return f(10 ** log_x)
log_result, comps = binary_search_min(f_log, log_left, log_right, epsilon)
return 10 ** log_result, comps
# Example: find optimal learning rate
def loss_at_lr(lr):
# Dummy loss function (imagine this is your actual training loss)
return (np.log10(lr) + 3.5) ** 2 + 0.1
optimal_lr, comps = log_binary_search(loss_at_lr, -6, 0)
print(f"Optimal LR: {optimal_lr:.2e} (found in {comps} comparisons)")
This outputs something like Optimal LR: 3.16e-04 in 20-25 comparisons.
Comparison Summary
| Metric | Binary Search | Ternary Search |
|---|---|---|
| Iterations to converge | binary | |
| Function evaluations/iter | 2 | 2 |
| Comparisons/iter | 1 | 1 |
| Total comparisons | ~31 (for ) | ~35 (same params, +13%) |
| Wall-clock (Rosenbrock) | 18.3 ms | 22.6 ms (+23%) |
| Handles flat minima | No (can stall) | Yes |
| Can reuse evaluations | Sometimes (optimized impl.) | No |
Real Interview Scenario
If an interviewer asks “find the minimum of a unimodal function”, don’t reach for ternary. Ask:
- Can I compute the derivative or check monotonicity? → Use binary.
- Is the function cheap to evaluate? → Either works, but binary is simpler.
- Is the function expensive (e.g., runs a simulation)? → Definitely binary if derivatives available, or golden-section if not.
The only time I’d code ternary in an interview is if the interviewer explicitly asks for it, or if the function is black-box and you’re trying to show off knowledge of alternative methods. Even then, I’d mention “but binary/golden-section is faster for most cases.”
One more thing: if you’re searching for a maximum instead of minimum, just negate the function. Don’t rewrite your search logic.
def binary_search_max(f, left, right, epsilon=1e-9):
"""Find maximum by negating the function and finding minimum."""
result, comps = binary_search_min(lambda x: -f(x), left, right, epsilon)
return result, comps
Clean and avoids sign-flip bugs.
FAQ
Q: When should I actually use ternary search over binary?
Realistically? Almost never. The only valid case is when you have a provably unimodal function, can’t compute derivatives, and don’t mind the 20-30% performance hit. Golden-section search is still better in that scenario. I’ve used ternary exactly once in production—optimizing a hyperparameter where the loss landscape was smooth but I didn’t trust automatic differentiation. It worked, but golden-section would’ve been more efficient.
Q: Does ternary search work on discrete arrays like binary search does?
Yes, but with caveats. On discrete unimodal arrays (e.g., [5, 3, 2, 1, 2, 4, 7] finding the minimum), ternary compares arr[left + n//3] vs arr[right - n//3] and prunes one-third of the array. It needs iterations versus binary’s . Same performance penalty as the continuous case. Stick with binary unless the problem explicitly asks for ternary.
Q: What if my function isn’t actually unimodal—will these algorithms break?
Both will confidently return a local minimum, not necessarily the global one. If you suspect multiple peaks/valleys, use a global optimizer (simulated annealing, genetic algorithms, or just grid search if the space is small). Binary and ternary search assume exactly one extremum. Violate that assumption, and you’ll get garbage results with no warning. Always validate unimodality first—plot the function or check that it’s convex/concave mathematically.
Algorithm Classification
- Type: Divide and conquer, optimization
- Time Complexity: Binary , Ternary
- Space Complexity: for both (iterative implementations)
- Related Algorithms: Golden-section search, bisection method, gradient descent
- Best Alternative: Golden-section search when derivatives unavailable; gradient-based methods when derivatives are cheap
Why This Still Matters
You’d think in 2026 we’d just throw everything at gradient descent and call it a day. But derivative-free optimization is alive and well: hyperparameter tuning (Optuna uses quasi-random search, not gradients), control systems where you’re tuning PID gains on a physical robot, or optimizing compiler flags where the “function” is build time.
Binary search on unimodal functions is one of those rare algorithms that’s both simple and optimal. Ternary is clever but slower. In an interview, clarity beats cleverness.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,817 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (954 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (783 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (711 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (559 views)