DEV Community

Cover image for Off-by-1 again in your LeetCode binary search solution?
Dash One
Dash One

Posted on • Edited on • Originally published at github.com

Off-by-1 again in your LeetCode binary search solution?

“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 or lo < hi?
  • hi = mid or hi = 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()
Enter fullscreen mode Exit fullscreen mode

⬇️ 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., last i where nums[i] <= target
  • .ceiling() → the first value that will drive the lambda to search down or stop. e.g., first i where nums[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();
}
Enter fullscreen mode Exit fullscreen mode

🧮 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();
}
Enter fullscreen mode Exit fullscreen mode

📦 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();
}
Enter fullscreen mode Exit fullscreen mode

🔍 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.


📦 More LeetCode binary search algorithms
📦 GitHub repo

Top comments (4)

Collapse
 
michelle_lei_7931f8c3717c profile image
Michelle Lei

Doesn't the forDoubles() need to provide a tolerance factor? Doubles aren't for equality check?

Collapse
 
fluentfuture profile image
Dash One • Edited

The forDoubles() doesn't use the usual (lo + hi) / 2 or lo + (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() and longBitsToDouble() to calculate the median, and uses Math.nextDown(double) and Math.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.

Collapse
 
fluentfuture profile image
Dash One

That said, in the "locator" lambda that returns -1, 1 or 0, you can certainly use a tolerance factor when comparing the (lo, mid, hi) with the search target.

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

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