好好学Java:从零基础到项目实战
上QQ阅读APP看书,第一时间看更新

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判断,是因为要校验迭代前后的xmxm+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次方根,有兴趣的读者可实践一下。