Solve LeetCode Binary Search Problems Using the BinarySearch class - google/mug GitHub Wiki
🧭 Solving LeetCode with BinarySearch
Binary search is a classic algorithm—but writing it correctly is surprisingly hard:
- Off-by-one errors are common
- Edge cases are messy
- Loop termination and insertion logic are easy to get wrong (infinite loop!)
- Be careful with overflow
This post shows how to solve three representative LeetCode problems using the safe and declarative BinarySearch
class — with no manual loop logic, and no risk of infinite loops.
🔍 Understanding insertionPointFor(...)
and How It Works
At the core of BinarySearch.Table
is this method:
insertionPointFor((lo, mid, hi) -> ...)
You're responsible for telling the search which direction to go at each probe point mid
.
TIP: For everyday use, there are simpler methods like find()
and rangeOf()
. The insertionPointFor()
is for advanced binary search problems.
BinarySearch.Table
supports two distinct binary search patterns:
insertionPointFor((lo, mid, hi) -> compare(target, mid));
This is the traditional form of binary search—looking for an exact match in a sorted array or list.
Use this when you're trying to locate a specific value.
Returning 0
indicates a match, < 0
means go left, and > 0
means go right.
insertionPointFor((lo, mid, hi) -> condition(mid) ? -1 : 1);
In this form, you're not searching for a specific value, but for the minimum or maximum value for a condition to still hold.
This works best when:
- The condition is monotonic (once true, always true—or once false, always false)
- You're trying to solve optimization problems like:
- The smallest feasible solution
- The largest possible value before something breaks
For example:
insertionPointFor((lo, mid, hi) -> canShipWithinDays(mid) ? -1 : 1);
This guides the search toward the smallest x
where the condition holds, i.e., the minimum required capacity.
Return -1
to search left (try smaller), 1
to search right (try larger).
No need to return 0
in this style—you only care about direction, not equality.
Find the minimum eating speed that allows Koko to finish all bananas within
h
hours.
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)
// floor() is the max speed that can't finish; ceiling() is the min that can
.ceiling();
}
boolean canFinish(List<Integer> piles, int speed, int h) {
int time = 0;
for (int pile : piles) {
time += (pile + speed - 1) / speed;
}
return time <= h;
}
Compute
floor(sqrt(x))
without using built-in functions.
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();
}
This returns the largest r
such that r² <= x
.
Split
nums[]
intok
parts, minimizing the largest sum among parts.
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)
// floor is the largest that can't split, ceiling is the min that can
.ceiling();
}
boolean canSplit(int[] nums, int maxSum, int k) {
int count = 1, sum = 0;
for (int num : nums) {
if (sum + num > maxSum) {
count++;
sum = 0;
}
sum += num;
}
return count <= k;
}
We use .ceiling()
to find the smallest feasible maximum subarray sum.
Each BinarySearch.Table
variant should match the nature of the problem:
Problem | Domain Type | Comparison Strategy | Return | Reason |
---|---|---|---|---|
Koko Eating Bananas | forInts |
canFinish(speed) ? -1 : 1 |
.ceiling() |
smallest speed that canFinish() |
Integer Square Root | forInts |
Long.compare(x, (long) mid * mid) |
.floor() |
largest number n where n*n <= x |
Split Array Largest Sum | forInts |
canSplit(maxSum) ? -1 : 1 |
.ceiling() |
smallest sum that canSplit() |
-
forInts()
when the domain is naturally integer-bounded. There are alsoforLongs()
andforDoubles()
. -
forDoubles()
can be used to search the entire double value space. IEEE 754 guarantees convergence within 64 iterations without needing an epsilon. - For more LeetCode binary search solutions, see
LeetCodeTest
.
Obviously you can't directly use it on leetcode (there's no Maven to pull in third-party libs). But you could still use it locally as a prototyping tool.
Explore your binary search idea quickly, make sure the logic is sound, then convert to a manual loop. It also helps you think in isolation.
For interviewers: are you evaluating the candidate's thinking process and abstraction capability? If so, being able to express the algorithm clearly (in terms of BinarySearch
) already proves that. If you rarely have to deal with this level of nuances and edge cases at work (such code would require extensive reviews), why should the candicate?
Mastering the thinking model of binary search is absolutely worthwhile. The ability to structure a search around monotonicity and directional reasoning is a form of higher-order abstraction that's fun and useful.
But you shouldn't have to spend your time wrestling with
off-by-one bugs, loop bounds, or whether to do high = mid
or low = mid + 1
.
Those are tedious, error prone and anything but fun (nor practically useful).
They drag you down into the mud of mind-numbing, imperative twiddling.
The BinarySearch class aims to get those details done once and for all, so you can focus on the fun and creative part of binary search.