“Although the basic idea of binary search is straightforward, the details are surprisingly tricky...
In fact, binary search has been incorrectly implemented in textbooks, published code, and production software for decades.”
— Donald Knuth, The Art of Computer Programming, Vol. 3“The truth is, very few correct versions have ever been published, at least in mainstream programming languages.”
— Joshua Bloch, Google Research Blog (2006)
Binary search is a beautiful algorithm — but implementing it is full of sharp edges:
- off-by-one errors
-
lo <= hi
orlo < hi
? -
hi = mid
orhi = mid - 1
? - mid overflow
- loop doesn't stop
The BinarySearch
class helps to abstract away those nuances.
🧠 How it Works
You provide a lambda that tells the algorithm which direction to search. Just return:
-
-1
→ search left -
1
→ search right -
0
→ stop (target found)
It follows the same contract as Comparator
.
BinarySearch.forInts(Range.closed(min, max))
.insertionPointFor((lo, mid, hi) -> ...)
.floor(); // or .ceiling()
⬇️ floor() or ⬆️ ceiling()?
Once the search lands on an insertion point, you choose:
-
.floor()
→ the last value that will drive the lambda to search up or stop. e.g., lasti
wherenums[i] <= target
-
.ceiling()
→ the first value that will drive the lambda to search down or stop. e.g., firsti
wherenums[i] >= target
🍌 LeetCode 875: Koko Eating Bananas
int minSpeed(List<Integer> piles, int h) {
return BinarySearch.forInts(Range.closed(1, max(piles)))
// if Koko can finish, eat slower, else eat faster
.insertionPointFor(
(lo, speed, hi) -> canFinish(piles, speed, h) ? -1 : 1)
// ceiling() is the first value that canFinish()
.ceiling();
}
🧮 LeetCode 69: Integer Square Root
int mySqrt(int x) {
return BinarySearch.forInts(Range.closed(0, x))
// if square is larger, try smaller sqrt
.insertionPointFor(
(lo, mid, hi) -> Long.compare(x, (long) mid * mid))
// floor() is the max whose square <= x
.floor();
}
📦 LeetCode 410: Split Array Largest Sum
int splitArray(int[] nums, int k) {
int total = Arrays.stream(nums).sum();
int max = Arrays.stream(nums).max().getAsInt();
return BinarySearch.forInts(Range.closed(max, total))
// if canSplit, try smaller sum, else try larger sum
.insertionPointFor(
(lo, maxSum, hi) -> canSplit(nums, maxSum, k) ? -1 : 1)
// ceiling is the min sum that can split
.ceiling();
}
🔍 TL;DR: Logic Summary
Problem | Search Domain | Predicate | Result |
---|---|---|---|
Eating bananas | forInts |
canFinish(speed) ? -1 : 1 |
.ceiling() |
Square root | forInts |
compare(x, mid²) |
.floor() |
Split array | forInts |
canSplit(sum) ? -1 : 1 |
.ceiling() |
🤔 Can I Use it on LeetCode?
Not directly. LeetCode doesn’t support third-party libraries.
But you can use it to:
- Quickly prototype and verify your binary search algorithm.
- Divide and conquer. Avoid wrestling with bugs and boilerplate until you know your
canSplit()
,canFinish()
logic works.
Top comments (4)
Doesn't the forDoubles() need to provide a tolerance factor? Doubles aren't for equality check?
The forDoubles() doesn't use the usual
(lo + hi) / 2
orlo + (hi - lo) / 2
to calculate the mid.Doing that would cause infinite loop due to double precision issues. And will also require hundreds of rounds to converge for large double numbers.
Instead, it uses bit operations with
doubleToRawLongBits()
andlongBitsToDouble()
to calculate the median, and usesMath.nextDown(double)
andMath.nextUp(double)
in place of+1
and-1
.The IEEE 754 guarantees convergence within 64 iterations without needing an epsilon.
The upside is that the search result is guaranteed to be the most precise double number representable by a Java double number.
That said, in the "locator" lambda that returns
-1
,1
or0
, you can certainly use a tolerance factor when comparing the(lo, mid, hi)
with the search target.yes sir this kind of breakdown saves so much pain tbh - ever wonder if all code bugs just come down to missing little details like these