1.概述

在处理大数字时,我们常常受限于intlong类型的大小。Java通过BigInteger类提供了一种解决方案。然而,有时API并不支持我们希望使用的所有算术运算。

求一个大数的平方根虽然常见,但往往颇具挑战性。本教程将介绍如何实现这一操作,并分析各种方法的优缺点。

2. 计算平方根

让我们来看看几种计算方法。

2.1. Java 9 BigInteger API

首先,我们从Java 9开始,它引入了最直接的方法。从这个版本开始,BigInteger提供了两个有用的方法:sqrt()sqrtAndReminder()。我们先来看第一个:

BigInteger integer = new BigInteger(bigDecimalNumber);
BigInteger root = integer.sqrt();

第二个方法类似,但提供了关于余数的额外信息:

BigInteger integer = new BigInteger(bigDecimalNumber);
BigInteger[] rootAndReminder = integer.sqrtAndRemainder();

这两个方法提供了简单直观的计算根的方法。然而,它们需要特定的Java版本,对于某些应用可能是个问题。

2.2. Guava的BigIntegerMath

另一种不升级Java版本的方法是使用Guava库

BigInteger integer = new BigInteger(bigDecimalNumber);
BigInteger root = BigIntegerMath.sqrt(integer, RoundingMode.DOWN);

这个方法接受RoundingMode参数来确定舍入逻辑。Guava库没有提供简单的方式来获取余数,如果需要,我们需要额外步骤:

BigInteger integer = new BigInteger(bigDecimalNumber);
BigInteger root = BigIntegerMath.sqrt(integer, RoundingMode.DOWN);
BigInteger reminder = integer.subtract(root.multiply(root));

2.3. NewtonPlus算法

虽然可以自定义实现大整数平方根的查找,但通常不建议这样做。自定义实现往往复杂度较高,处理效率较低。有一些简单但效率低下的算法,如二分查找法(Java查找平方根是否为整数)和牛顿法(Java查找平方根是否为整数`)。

然而,有一种算法NewtonPlus在标准Java和Guava实现上表现出更好的性能。这个算法由Ryan Scott White提出,基于牛顿法,对于大于5 x 10^14的数字显示出性能提升。

性能比较

为了比较Java、Guava库、NewtonPlus和牛顿法的性能差异,我们将在三个不同的值上运行性能测试:

private static final String BIG_NUMBER = "1797693130000000000000000000000..." // 309 digits
private static final String VERY_BIG_NUMBER = "32473927492374927934284792..." // 1802 digits
private static final String INSANELY_BIG_NUMBER = "3247392749237492793428..." // 3604 digits

执行JMH基准测试后,我们得到以下结果:

Benchmark                                                  (number)              Mode  Cnt       Score   Error  Units
BigIntegerSquareRootBenchmark.calculateRootWithNewton      BIG_NUMBER           thrpt         2668.642          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithJava        BIG_NUMBER           thrpt        25417.428          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithGuava       BIG_NUMBER           thrpt       144117.671          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithNewtonPlus  BIG_NUMBER           thrpt       308933.077          ops/s

BigIntegerSquareRootBenchmark.calculateRootWithNewton      VERY_BIG_NUMBER      thrpt           33.627          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithJava        VERY_BIG_NUMBER      thrpt         1376.668          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithGuava       VERY_BIG_NUMBER      thrpt         5349.748          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithNewtonPlus  VERY_BIG_NUMBER      thrpt        12283.677          ops/s

BigIntegerSquareRootBenchmark.calculateRootWithNewton      INSANELY_BIG_NUMBER  thrpt            9.135          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithJava        INSANELY_BIG_NUMBER  thrpt          553.475          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithGuava       INSANELY_BIG_NUMBER  thrpt         1713.520          ops/s
BigIntegerSquareRootBenchmark.calculateRootWithNewtonPlus  INSANELY_BIG_NUMBER  thrpt         3252.308          ops/s

NewtonPlus方法表现出最佳性能,对于需要大量此类计算的应用来说可能是不错的选择。然而,如果在代码库中添加高度优化的自定义代码不太吸引人,我们可以选择性能相对较好的Guava的BigIntegerMath

另外,像牛顿这样的简单算法效率低下,应避免使用。总的来说,二分查找法的性能不如牛顿法,因为它收敛速度较慢。

结论

在处理大数时,我们需要一种方便的方式来在其上执行标准数学运算。从数学角度看,无论数字大小如何,运算都是相同的,但计算机科学会带来一些限制。这就是为什么我们需要特定的库和算法来处理BigIntegers

根据具体任务的需求,我们可以选择不同的解决方案,从标准库到针对应用需求精心调整的自定义解决方案。然而,过早优化通常并不好,甚至有害。因此,我们在选择时应保持合理。

如往常一样,所有的代码可以在GitHub上找到。