4.4.2 利用牛顿迭代法求大数开方
自从把一段功能独立的代码剥离为可以复用的方法,计算机语言的编码效率顿时得到了飞跃的提升,因为许多数学函数都能书写为公共方法给外部调用。除了前面介绍的阶乘函数之外,开方函数也能编写为公共的开根号方法,尽管系统自带的Math库已经提供了sqrt这么一个开平方函数,但JDK的源码并非给出该方法的Java实现代码,故而我们有必要了解一下如何通过Java代码实现开根号方法。
根据之前介绍的牛顿迭代法,能够得出求平方根的迭代式子为。由于多次迭代的过程可以借助循环语句完成,因此可通过while关键字编写求方根的方法定义,具体的方法代码示例如下(完整代码见本章源码的src\com\method\BigNewton.java):
// 计算双精度数的平方根 private static double sqrtByDouble(double number) { double Xm=number; // 每次迭代后的数值 while (true) { double lastXm=Xm; // 上次迭代的平方根 Xm=(Xm + number/Xm) / 2; // 本次迭代后的平方根 // 迭代前后的两个平方根相等,表示已经达到变量精度,再计算下去就没意义了 if (Xm >= lastXm) { break; } } return Xm; }
上述代码之所以添加了if判断,是因为要校验迭代前后的xm与xm+1是否相等,如果二者等值,便说明本次迭代已经达到了双精度类型的精度范围,此时继续迭代也无法获得更好的方根精度,当然要退出循环以避免无谓的运算操作。注意到if判断语句位于while循环的末尾,且满足if条件时就退出循环,因而这段while语句可以改写为do/while语句,改写后的方法代码如下:
// 计算双精度数的平方根(使用do/while循环) private static double sqrtByDoubleWithDo(double number) { double Xm=number; // 每次迭代后的数值 double lastXm=Xm; // 上次迭代的平方根 do { lastXm=Xm; // 保存上次迭代的平方根 Xm=(Xm + number/Xm) / 2; // 本次迭代后的平方根 } while (Xm < lastXm); // 只有迭代前后的两个平方根不等的时候,才要继续执行循环 return Xm; }
无论是采用while循环,还是采用do/while循环,两个方法计算出来的方根结果是一样的。以下是外部调用其中一个开根号方法的代码例子,准备计算整数2的平方根:
// 测试双精度数的开方运算 private static void testSqrtByDouble() { double number=2; // 需要求方根的数字 double root=sqrtByDouble(number); // 计算双精度数的平方根 System.out.println("双精度数开方运算,原始数字=" + number + ", 它的平方根=" + root); }
运行上面的开方代码,观察到下面的日志信息,可见通过自定义的方法也能正确求得数字的 方根。
双精度数开方运算,原始数字=2.0, 它的平方根=1.414213562373095
然而受到存储空间限制,双精度类型只能表达到小数点后面15位,再往后的小数位就无能为力了。虽然这点误差在日常生活中算不了什么,但在精细的金融领域和精密的工程领域是不可接受的,为了计算出足够精确的方根数值,需要采取大小数类型进行开方运算。由于大小数的除法运算不允许出现无限小数,必须在调用相除方法divide时指定保留位数,因此还要引入精度工具MathContext来设定精度参数。同样利用牛顿迭代法编写大小数的开方运算,详细的实现代码如下(完整代码见本章源码的src\com\method\BigNewton.java):
// 计算大小数的平方根 private static BigDecimal sqrtByBigDecimal(BigDecimal number, int precision){ BigDecimal two=BigDecimal.valueOf(2); // 指定运算精度,保留若干位数,最后一位四舍五入取整 MathContext mc=new MathContext(precision, RoundingMode.HALF_UP); if (number.compareTo(BigDecimal.ZERO) <= 0) { // 0和负数不允许开方 return BigDecimal.valueOf(0); } else { BigDecimal X=number; // 上次迭代的平方根 // 下面利用牛顿迭代法计算某个大小数的平方根 while (true) { // 简化之后求平方根的迭代式子:Xm=(Xm + number/Xm) / 2 BigDecimal Xm=(X.add(number.divide(X, mc))).divide(two, mc); // 如果运算前后的结果相等,就跳出循环。因为已经达到运算精度,再计算下去也无用 if (X.equals(Xm)) { break; } X=Xm; // 保留本次迭代后的方根 } return X; } }
仍然以2为底数计算它的方根,调用大小数的求方根方法时,假定保留小数点后面100位,则外部的调用代码可书写为下面这样:
// 测试大小数的开方运算 private static void testSqrtByBigDecimal() { BigDecimal number=BigDecimal.valueOf(2); // 需要求方根的数字 // 求得的平方根保留小数点后面100位 BigDecimal root=sqrtByBigDecimal(number, 100); // 计算大小数的平方根 System.out.println("大小数开方运算,原始数字=" + number + ", 它的平方根=" + root); }
运行上面的大小数开方代码,观察到下面的日志信息:
大小数开方运算,原始数字=2,它的平方根=1.41421356237309504880168872420969807856967187537694807 3176679737990732478462107038850387534327641573
由日志结果可见,大小数保留了足够的位数,再也不用担心那些专业领域的数值偏差了。既然通过大小数能够求得更精确的平方根,那么也能求得更精确的三次方根、四次方根乃至N次方根,有兴趣的读者可实践一下。