Finding Exact Roots

You know how to binary search integers. Finding the integer square root of 49 is just boundary search on [0, 49] — the search space is finite. At each step, lo and hi are integers, and the gap between them shrinks by at least 1. Eventually lo meets hi, and you are done.

But what about sqrt(2)? That is not an integer. The answer is an infinite decimal: 1.41421356...

The search space is the real number line between 1.0 and 2.0. There are infinitely many values in that interval. lo and hi are floating-point numbers, and every split creates two more infinitely-large half-intervals. Between 1.4 and 1.5 there is 1.41. Between 1.41 and 1.42 there is 1.414. No matter how tight the interval gets, there are always more values to check.

The integer search had a built-in stopping condition: the gap reaches zero because integers have a minimum spacing of 1. Floating-point numbers have no such minimum. The gap between lo and hi can shrink to 0.0000001, then 0.00000001, then 0.000000001 — it halves forever but never reaches zero. Your while (lo < hi) loop worked perfectly on integers. On real numbers, it becomes while (true).

How does binary search handle a search space that has no “last element”?

Two Worlds

Try both. First the integer case, then the real-number case. Watch what happens to the gap between lo and hi in each.

1 / 4
Find sqrt(49) on integers [0..49]. Where would the search look first?
051015202530354045lohi25
1
function intSqrt(x: number): number {
2
  let lo = 0, hi = x;
3
  while (lo < hi) {
4
    const mid = lo + Math.ceil((hi - lo) / 2);
5
    if (mid <= x / mid) lo = mid;
6
    else hi = mid - 1;
7
  }
8
  return lo; // lo === hi === 7
9
}

mid = 25. Which HALF contains sqrt(49)?

In the integer case, the search converges cleanly — the gap hits zero and the loop exits. In the real-number case, the gap gets smaller and smaller but never reaches zero. The interval goes from [1.0, 2.0] to [1.0, 1.5] to [1.25, 1.5] to [1.375, 1.5]... always halving, never finishing.

The epsilon approach — while (hi - lo > 1e-9) — seems like the obvious fix. But it has a subtle failure mode that has burned thousands of competitive programmers.

Consider searching for sqrt(2). The answer is around 1.414. An absolute epsilon of 1e-9 works fine here — the initial interval is [1.0, 2.0], the gap is 1.0, and after about 30 iterations of halving you are below 1e-9. Clean convergence.

Now consider sqrt(1e18). The answer is around 1e9. The initial interval is [0, 1e18]. After 60 iterations the gap is roughly 1e18 / 2^60 ≈ 0.87 — still larger than 1e-9. That is fine, it just needs more iterations. But here is where it breaks: 64-bit floating-point numbers have about 15-16 significant digits of precision. At a value of 1e9, the smallest representable difference between two floats is about 1e-7. An epsilon of 1e-9 is below the precision floor at that magnitude. The condition hi - lo > 1e-9 might never become false — not because the math is wrong, but because the hardware cannot represent numbers that close together at that scale. The loop stalls, or oscillates, or produces subtly incorrect results depending on the platform.

The relative epsilon approach — while ((hi - lo) / hi > 1e-9) — fixes the large-value problem but introduces a new one: division by a value near zero amplifies rounding errors. If hi is close to 0, the relative gap explodes to infinity even when the absolute gap is tiny.

There is a simpler solution, one that sidesteps both failure modes entirely. You already know the answer is within the initial interval. Each iteration halves the gap. After 100 iterations, the gap has been multiplied by 2^-100 ≈ 7.9e-31. That is more precision than any floating-point format can represent. The loop always terminates because the iteration count is fixed. No epsilon to tune, no precision traps, no edge cases.

The Rule

You discovered it yourself:

Integer binary search terminates naturally. lo and hi are integers, so the gap shrinks by at least 1 each iteration. When lo >= hi, you are done. The risk is off-by-one, not convergence.

Real-number binary search never naturally terminates — you can always split the interval one more time. You need an explicit stopping condition. The question is: which one?

The naive approach — while (hi - lo > epsilon) — is fragile. An absolute epsilon like 1e-9 gives terrible relative precision when the values are large. And relative epsilon (hi - lo) / hi fails near zero where division amplifies errors.

The robust approach: fixed iteration count. for (let i = 0; i < 100; i++) gives 2^-100 ~ 1e-30 precision factor, regardless of the input range. It is simpler, more reliable, and the standard approach in competitive programming.

1
// Integer domain: natural termination
2
let lo = 0, hi = n;
3
while (lo < hi) {
4
  const mid = lo + Math.floor((hi - lo) / 2);
5
  if (mid * mid <= target) lo = mid + 1;
6
  else hi = mid;
7
}
8
return lo - 1;  // largest integer where i*i <= target
9
10
// Real domain: fixed iteration count
11
let lo = 0, hi = target;
12
for (let i = 0; i < 100; i++) {
13
  const mid = (lo + hi) / 2;  // no floor needed
14
  if (mid * mid <= target) lo = mid;
15
  else hi = mid;
16
}
17
return lo;  // precise to ~1e-30 relative error

Notice the real-number version uses lo = mid and hi = mid — no +1 or -1. On continuous domains there are no “adjacent” values to skip to, so you just split the interval exactly in half. The mid - 1 and mid + 1 adjustments exist because integers have gaps between them. Real numbers do not, so the adjustment disappears.

Also notice: no Math.floor. Integer binary search uses floor division to avoid non-integer midpoints. Real-number search uses plain (lo + hi) / 2 because the midpoint is allowed to be any real value. The arithmetic simplifies when the domain is continuous.

Two domains. Two failure modes. Two templates:

IntegerReal-number
Terminationlo >= hi (gap reaches 0)Fixed iterations (for i < 100)
Mid updatemid + 1 / mid - 1mid (no adjustment)
Mid formulalo + Math.floor((hi - lo) / 2)(lo + hi) / 2
RiskOff-by-one errorsEpsilon precision traps

The real-number pattern appears in more problems than you might expect. LC 69 (Sqrt(x)) is the integer version, but problems like “find the minimum speed to arrive on time” (LC 1870), “magnetic force between two balls” (LC 1552), and many geometry problems use continuous binary search. Whenever the answer is a real number and the feasibility predicate is monotonic, the 100-iteration template is your tool. It is also the standard approach in competitive programming for problems labeled “binary search on real numbers” — Codeforces, AtCoder, and ICPC judges all accept this pattern.

The decision rule is simple: can your answer be a fraction? Use fixed iterations. Is your answer always a whole number? Use lo < hi. That single question determines which template to reach for.