Here I present simple algorithms BigIntSqrt and BigIntCbrt for computing exact integer values (truncated towards zero) of square and cube roots of arbitrary integers using only integer arithmetic. These can be used to implement BigInt
equivalents of Math.sqrt
and Math.cbrt
in ECMAScript.
Both algorithms are efficient, taking only log(log(n)) operations to compute square or cube roots of n.
I'm also including proofs that these algorithms compute the exact results for all inputs:
Runnable code is in roots.js.
The goal is to get BigIntSqrt and BigIntCbrt included in the ECMAScript language under some suitable name in the effort to extend Math
library functions such as Math.abs
to also cover BigInt
(the exact form and whether they'll be overloads or separate functions are to be decided). This is preferable to having users blindly copy snippets they find “on the net”. For example, at the time of this writing the top Google search result for "javascript bigint sqrt" produces a link to a buggy and needlessly slow ECMAScript algorithm that sometimes produces incorrect answers, such when computing the square root of 4.
— Waldemar Horwat — May 2022
All math operations have their usual mathematical meanings on real numbers.
- As is the convention in mathematics, a positive real number or integer means that it's greater than zero; similarly a negative real number or integer means that it's less than zero. The integer or real number zero is neither positive nor negative.
- ⌊x⌋ is the floor of the real number x (i.e. truncated towards -∞ to an integer):
⌊7.1⌋ = 7, ⌊–3.2⌋ = –4, ⌊5⌋ = 5, ⌊–2⌋ = –2. - [x] is the real number x truncated towards 0 to an integer:
[7.1] = 7, [–3.2] = –3, [5] = 5, [–2] = –2.
We can combine [x] with division to denote integer division truncating towards 0:
- is the quotient of x divided by y truncated towards 0 to an integer:
[17/5] = [3.4] = 3, [–7/2] = [–3.5] = –3, [10/2] = 5.
When x and y are integers, this is the same as ECMAScript'sBigInt
division of x and y.
When x ≥ 0 and y > 0, the result of x/y is nonnegative, so truncating it towards 0 is the same as truncating it towards -∞.
- In such nonnegative cases we'll sometimes use instead of . In those cases they're equivalent and both denote ECMAScript's
BigInt
division of x and y.
We'll need a helper function BigIntLog2 that, given a positive integer n, returns the position of its most significant set bit when expressed in binary. For example, BigIntLog2(1) = 0, BigIntLog2(2) = 1, BigIntLog2(255) = 7, BigIntLog2(256) = 8.
Mathematically BigIntLog2 is defined as
To put it another way, BigIntLog2(n) is the integer w that satisfies 2w ≤ n < 2w+1.
We'll assume we have an efficient ECMAScript implementation of BigIntLog2 that takes a positive BigInt
and returns a BigInt
greater than or equal to zero.
We implement BigIntSqrt in ECMAScript as follows. For simplicity we assume that the argument n has already been checked to be a BigInt
.
function BigIntSqrt(n) {
if (n < 0n)
throw RangeError("Square root of negative BigInt");
if (n === 0n)
return 0n;
const w = BigIntLog2(n); // BigIntLog2 returns a BigInt
let x = 1n << (w >> 1n); // x is the initial guess x0 here
let next = (x + n/x) >> 1n;
do {
x = next;
} while ((next = (x + n/x) >> 1n) < x);
return x;
}
All numbers are nonnegative, so the right-shifts by 1 are equivalent to dividing by 2.
If we step through BigIntSqrt(123456n)
, we get the following values of x, w, and the final next:
- w =
16n
- x0 =
256n
- x1 =
369n
- x2 =
351n
- next =
351n
and the result is x2 = 351n
.
Occasionally the final next can be greater than the last value of x, as in BigIntSqrt(80n)
:
- w =
6n
- x0 =
8n
- x1 =
9n
- x2 =
8n
- next =
9n
where the result is x2 = 8n
.
The algorithm converges rapidly, using log(log(n)) iterations. Let's take the square root of a googol:
BigIntSqrt(10n**100n)
- w =
332n
- x0 =
93536104789177786765035829293842113257979682750464n
- x1 =
100223346596432806305328643989950694205293778977332n
- x2 =
100000248862684355295361037376723795016205636438280n
- x3 =
100000000000309662407688436639554331697921065069655n
- x4 =
100000000000000000000000479454033675513027179143720n
- x5 =
100000000000000000000000000000000000000000000001149n
- x6 =
100000000000000000000000000000000000000000000000000n
- next =
100000000000000000000000000000000000000000000000000n
with the correct answer x6 = 1050.
Let's figure out the exact values of the first 101 decimal digits of the square root of 2:
BigIntSqrt(2n * 10n**200n)
- w =
665n
- x0 =
8749002899132047697490008908470485461412677723572849745703082425639811996797503692894052708092215296n
- x1 =
15804375362388773670902839937288652325225438810014411485002799645096232357466133803201506572647827944n
- x2 =
14229549421664044022085597366571792103664648001192143121732144276497668172208742442918179650436359389n
- x3 =
14142404120358730362124525665217767401060790557815075015694913950869126908913392090386697015371402453n
- x4 =
14142135626279684017183484419478198323023092160355018411871574810862373478578236444214290814824160615n
- x5 =
14142135623730950488246557030817812967256722016462383289181725954672117314556926820527863606311500271n
- x6 =
14142135623730950488016887242096980785698583684684248602001325910890328013825461394333000393858369222n
- x7 =
14142135623730950488016887242096980785696718753769480731766797379907324784621070511468589068012098747n
- x8 =
14142135623730950488016887242096980785696718753769480731766797379907324784621070388503875343276415727n
- next =
14142135623730950488016887242096980785696718753769480731766797379907324784621070388503875343276415727n
The result x8 is ⌊10100 √2⌋, so √2 = 1.4142135623730950488016887242096980785696718753769480731766797379907324784621070388503875343276415727…
We can add an optimization that computes BigIntSqrt directly using floating-point arithmetic for n small enough that Math.sqrt
on n converted to a Number
is accurate to within a few least significant bits. The threshold of what counts as "small enough" varies depending on the implementation accuracy of Math.sqrt
, but 244 should be safe for any reasonable implementation.
function BigIntSqrt(n) {
if (n < 0n)
throw RangeError("Square root of negative BigInt");
if (n < 0x100000000000n) { // 2^44
return BigInt(Math.floor(Math.sqrt(Number(n) + 0.5)));
}
... rest as before
Adding 0.5 to n avoids potential problems if n is a perfect square of an integer s2 but Math.sqrt
is slightly inaccurate and produces an approximation that's slightly below s.
We implement BigIntCbrt as follows, which produces the cube root of n truncated towards zero. Once again we assume that the argument n has already been checked to be a BigInt
.
function BigIntCbrt(n) {
if (n < 0n)
return -BigIntCbrt(-n);
if (n === 0n)
return 0n;
const w = BigIntLog2(n); // BigIntLog2 returns a BigInt
let x = 1n << (w / 3n); // x is the initial guess x0 here
let next = (2n*x + n/(x*x)) / 3n;
do {
x = next;
} while ((next = (2n*x + n/(x*x)) / 3n) < x);
return x;
}
If we step through BigIntCbrt(125n)
, we get the following values of x, w, and the final next:
- w =
6n
- x0 =
4n
- x1 =
5n
- next =
5n
and the result is x1 = 5n
.
Let's take the cube root of a googol:
BigIntCbrt(10n**100n)
- w =
332n
- x0 =
1298074214633706907132624082305024n
- x1 =
2843626090122429343812008194362090n
- x2 =
2307975193491828189478923582864817n
- x3 =
2164422626317660162834416053331641n
- x4 =
2154480709428036015136058273653333n
- x5 =
2154434691014844364863943029148908n
- x6 =
2154434690031883722207769275611571n
- x7 =
2154434690031883721759293566519350n
- next =
2154434690031883721759293566519350n
with the answer x7 = 2154434690031883721759293566519350, which is the first 34 digits of the decimal representation of the cube root of 10.
As with BigIntSqrt, we can do an optimization for small values of n:
function BigIntCbrt(n) {
if (n < 0n)
return -BigIntCbrt(-n);
if (n < 0x100000000000n) { // 2^44
return BigInt(Math.floor(Math.cbrt(Number(n) + 0.5)));
}
... rest as before
Now let's delve into how and why the BigIntSqrt and BigIntCbrt algorithms work.
The basic approach of computing the square or cube root of n is based on solving the equation x2 – n = 0 or x3 – n = 0 for a real number x. We can do this by starting with an initial guess x0 and then using a variant of Newton's method to refine it to produce successive approximations x1, x2, and so on until we find the desired answer. Let's first take a look at how Newton's method works on real numbers:
Given an approximation xi to a root of the equation f(x) = 0, Newton's method produces the next approximation
For computing square roots we're looking for roots of f(x) = x2 – n so Newton's method becomes
For cube roots we're looking for roots of f(x) = x3 – n, in which case Newton's method becomes
These will produce an infinite series of successively more accurate real number approximations of the square or cube root of n.
The standard Newton's method uses real numbers and produces an infinite series of approximations. Let's modify it to use only integer arithmetic to find integer square or cube roots truncated towards 0. Later we'll show that we'll arrive at the exact answer in finitely many operations. A similar algorithm for square roots is described on Wikipedia's entry on integer square roots but without the detailed proof of correctness.
The algorithm that implements BigIntSqrt(n) where n is a nonnegative integer can be stated mathematically as follows:
If n = 0, then return 0. Otherwise, let
For i = 0, 1, 2, 3, … compute the series
until we find the lowest k > 0 such that xk+1 ≥ xk. Return xk.
Later we will prove that our search for such a k terminates and that xk satisfies
The algorithm that implements BigIntCbrt(n) where n is any integer can be stated mathematically as follows:
If n = 0, then return 0.
If n < 0, then return –BigIntCbrt(–n).
Otherwise n is positive. Let
For i = 0, 1, 2, 3, … compute the series
until we find the lowest k > 0 such that xk+1 ≥ xk. Return xk.
Later we will prove that our search for such a k terminates and that xk satisfies
The initial guess x0 must be positive and should be close to the result. As proven below, the code presented here and the above formulas pick x0 to always be within a factor of 2 of the final result, which results in quick convergence. Were we to pick the initial guess x0 = 1 (as some web sites recommend), the algorithm would still work but converge slowly, for example taking 172 iterations to compute the square root of 10100 instead of the 6 iterations using the algorithm presented here.
Let's start with a few lemmas about the floor function.
Given a real number x, ⌊x⌋ is the unique integer i such that x = i + f and 0 ≤ f < 1. From that we can derive the following lemmas.
This is obvious from the definition of ⌊x⌋.
To prove this, let x = i + f where i is an integer and 0 ≤ f < 1. Then let i = jn + k where j and k are integers and 0 ≤ k ≤ n – 1. Then we have 0 ≤ k + f < n and thus
If all of a, b, c, and n are integers with b ≠ 0 and c > 0, we can eliminate the inner floor in:
Proof: By lemmas 1 and 2 we have
Recall that the series for computing BigIntSqrt(n) when integer n > 0 consists of
Also let's define
Given n ≥ 1, we have s ≥ 1 and x0 is an integer greater than 0. All subsequent terms of the series are also integers due to the definition of xi+1. Next we'll show by induction that all terms of the series after the zeroth one (i.e. xi with i > 0) are greater than or equal to s.
Suppose xi ≥ 1. We'll show that xi+1 ≥ s.
The square of any real number is nonnegative, so we have
We can divide both sides by the positive quantity 2xi and simplify to get
Taking the floor of both sides and then using lemma 3 we get
Thus xi+1 ≥ s ≥ 1, which completes the induction.
We already know that xi ≥ s for all i > 0. Now suppose xi > s for some i. We'll show that xi+1 < xi so the series is strictly decreasing as long as terms are greater than s.
xi and s are integers, so xi > s implies
By the definition of s, we get
Combining the above two inequalities yields
Squaring both sides (which is valid because the function f(a) = a2 is monotonically increasing for positive a) and then adding xi2 to both sides results in
Dividing both sides by the positive value 2xi we get
Applying lemma 3 yields the upper bound on xi+1.
Combining the upper bound with the lower bound, we get
There are only finitely many integers between s and xi so the series must decrease on each step (other than the zeroth because we don't necessarily have x0 > s) and eventually reach xk = s for some k. At that point the series cannot decrease further, so we can detect xk = s by looking for k > 0 and xk+1 ≥ xk.
Now let's show that the worst-case running time is O(log(log(n))) integer operations for positive n.
For each term in the xi series above let's introduce the concept of a corresponding relative error ei which indicates how much xi differs from √n. Define
or equivalently
First let's calculate the possible range of e0.
Applying lemma 2 to the definition of x0 have
By the properties of the floor function, we have
Exponentiation with a positive base is monotonically increasing, so we can exponentiate all three formulas while preserving the inequalities
which we can simplify using the properties of exponentiation and the definition of x0 to
Thus x0 is always within a factor of 2 of √n.
To compute bounds on e0 we divide all sides by √n and subtract 1 to get
and once again simplify and substitute the definition of e0 to get the bounds
Let's compute ei+1 in terms of ei. Combining the definition of ei+1, the recursion formula for xi+1, and xi = √n(1+ei) we have
Thus
Let's name the right side f(ei).
Its derivative is
so f(ei) is monotonically increasing for ei ≥ 0 and monotonically decreasing for –1 < ei ≤ 0 (f has a pole at ei = –1).
From the bounds on e0 we can compute the bounds on f(e0) and thus e1.
f(e0) is monotonically decreasing on that range so
Since ei+1 ≤ f(ei), we have
The algorithm terminates when we get ei ≤ 0 with i > 0. That's because ei ≤ 0 implies xi ≤ √n, but we earlier showed the lower bound xi ≥ s for i > 0, so xi is an integer such that
which requires xi = s.
If we get 0 < ei ≤ 1/√n with i > 0 then the algorithm must terminate on the next iteration. That's because 0 < ei implies s < xi while ei ≤ 1/√n implies
so xi is an integer such that
s is an integer so the only possibility is xi = s + 1, but we know from the lower and upper bounds that in this case
so xi + 1 = s
Recall the recursion relation
When ei is much greater than 1 this series decreases exponentially. For example, if ei = 2100, then ei+1 ≈ 299, ei+2 ≈ 298, and so on, taking roughly 100 iterations to reach 1. This is what would happen had we chosen a bad initial guess such as x0 = 1. Fortunately the algorithm presented here does not do that.
When ei is positive and less than 1, the series decreases doubly-exponentially (the exponential of an exponential). For example, if ei = 2–100, then ei+1 ≈ 2–201, ei+2 ≈ 2–403, and so on. Our algorithm operates in this regime. Let's formalize it.
We showed earlier that
We can prove by induction that, as long as the series hasn't reached the termination condition by going to zero or negative earlier, for i > 0 we have
The base case e1 is above. For the induction case,
If n = 1, x0 = x1 = 1 and we're done immediately. If n > 1, we're guaranteed that ei < 1/√n for some positive i no higher than log2(log2(√n)), and the algorithm must terminate either on that or the subsequent iteration. Thus, including the zeroth and the last iteration, the maximum running time is log2(log2(√n)) + 2 iterations, which is O(log(log(n)).
The series for computing BigIntCbrt(n) when integer n > 0 consists of
Also let's define
Given n ≥ 1, we have s ≥ 1 and x0 is an integer greater than 0. All subsequent terms of the series are also integers due to the definition of xi+1. Next we'll show by induction that all terms of the series after the zeroth one (i.e. xi with i > 0) are greater than or equal to s.
Suppose xi ≥ 1. We'll show that xi+1 ≥ s.
The left factor is always positive and the right factor is the square of a real number which is always nonnegative, so we have
Expanding the products and collecting terms yields
We can divide both sides by the positive quantity 3xi2 and simplify to get
Taking the floor of both sides and then using lemma 3 we get
Thus xi+1 ≥ s ≥ 1, which completes the induction.
We already know that xi ≥ s for all i > 0. Now suppose xi > s for some i. We'll show that xi+1 < xi so the series is strictly decreasing as long as terms are greater than s.
xi and s are integers, so xi > s implies
By the definition of s, we get
Combining the above two inequalities yields
Cubing both sides (which is valid because the function f(a) = a3 is monotonically increasing) and then adding 2xi3 to both sides results in
Dividing both sides by the positive value 3xi2 we get
Applying lemma 3 yields the upper bound on xi+1.
Combining the upper bound with the lower bound, we get
There are only finitely many integers between s and xi so the series must decrease on each step (other than the zeroth because we don't necessarily have x0 > s) and eventually reach xk = s for some k. At that point the series cannot decrease further, so we can detect xk = s by looking for k > 0 and xk+1 ≥ xk.
QED
The cube root running time analysis is analogous to the square root case, albeit with more complicated recursion formulas. The algorithm is still O(log(log(n))) with slightly different constants.