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

4.4.3 利用大数字求更精确的圆周率

既然开平方的运算代码可以提取为单独的公共方法,同样圆周率的求解代码也能提取为公共方法。利用割圆术逐次在圆圈内部切割内接的正N边形,由此求得的圆周率近似值将越来越逼近它的真实值。根据之前讲述的计算方式,可将迭代部分改写为如下的方法代码(完整代码见本章源码的src\com\method\ExactPai.java):

    // 计算粗略的圆周率
    private static double getPaiRough(int n) {
        int r=1;                      // 圆的半径
        int d=2 * r;                  // 圆的直径
        double edgeLength=r;          // 正n边形的边长
        long edgeNumber=6L;          // 正n边形的边数
        double π=edgeLength * edgeNumber / d;          // 正六边形对应的圆周率
        for (int i=0; i < n; i++) {                  // 利用for循环实现迭代功能
            edgeNumber=edgeNumber * 2;                  // 正n边形的边数乘2
            double gou=edgeLength / 2.0;              // 计算勾
            double gu=r - Math.sqrt(Math.pow(r, 2) - Math.pow(gou, 2));  // 计算股
            // 通过勾股定理求斜边长(勾三股四弦五),斜边长也是新正n边形的边长
            edgeLength=Math.sqrt(Math.pow(gou, 2) + Math.pow(gu, 2));
            // 正n边形的周长除以直径,即可得到近似的圆周率数值
            π=edgeLength * edgeNumber / d;
        }
        return π;
    }

接着外部调用上面的getPaiRough方法,想要计算双精度类型的圆周率数值,则调用代码示例如下:

    // 利用双精度数计算圆周率
    private static void calculateByDouble() {
        int n=60;  // double类型最多只能割60次
        long edgeNumber=(long) (Math.pow(2, 60) * 6);
        System.out.println("割圆次数=" + n + ", 内接正N边形的边数=" + edgeNumber);
        // 使用double类型,割圆术只能内接到正6917529027641081856边形(n=60)
        // 再往上割圆的话,Java程序就会错乱
        double π_rough=getPaiRough(n);
        System.out.println("粗略的圆周率数值=" + π_rough);
    }

运行以上的圆周率计算代码,观察到以下的日志信息:

割圆次数=60,内接正N边形的边数=6917529027641081856

粗略的圆周率数值=3.1415926535897936

注意到这里的割圆次数只有60次,却不能继续割下去了,因为再割下去程序就会计算得乱七八糟,原因有二:

(1)double类型最多表达到小数点后16位,再往后不但计算不精确,还会因超出精度范围而导致勾、股、弦计算错误。

(2)long类型也有数值范围限制,割圆次数较多的话,内接正N边形的边数将会超出long类型的表示范围。

为了解决上述两个问题,势必需要分别引入大小数BigDecimal和大整数BigInteger。可是纵然借助大小数和大整数把圆周率计算得更精确,这又有什么意义呢?毕竟初学者离工程等专业领域还远着呢。话虽如此,却也挡不住疯狂理工男的求知热情,日本有人出版了一本书,书名叫作《π》,这本书讲的就是圆周率小数点后面100万位的数字,而且还很畅销。既然人家这么有钻研精神,我们也不能落后,继续发扬老祖宗割圆术的光荣传统,结合勾股定理的神机妙算,一样能够将圆周率计算到小数点之后N位。

于是把原来通过双精度数计算圆周率的方法定义稍加改造,使用大小数类型替换掉双精度类型,并指定除法情况下的小数位精度,则可改造为如下的新式计算方法(完整代码见本章源码的src\com\method\ExactPai.java):

    // 计算精确的圆周率
    private static BigDecimal getPaiExact(int n) {
        BigDecimal two=BigDecimal.valueOf(2.0);
        BigDecimal r=BigDecimal.valueOf(1);              // 圆的半径
        BigDecimal d=r.multiply(two);                      // 圆的直径
        BigDecimal edgeLength=r;                          // 正n边形的边长
        BigDecimal edgeNumber=BigDecimal.valueOf(6);      // 正n边形的边数
        BigDecimal π=edgeLength.multiply(edgeNumber).divide(d);  // 正六边形对应的圆周率
        int precision=110;  // 默认保留小数点后面110位
        // 设定小数的精度,保留小数点若干位,最后一位四舍五入
        MathContext mc=new MathContext(precision, RoundingMode.HALF_UP);
        for (int i=0; i < n; i++) {  // 利用for循环实现迭代功能
            edgeNumber=edgeNumber.multiply(two);  // 正n边形的边数乘2
            BigDecimal gou=edgeLength.divide(two, mc);  // 计算勾
            BigDecimal gu=r.subtract(sqrt(r.pow(2).subtract(gou.pow(2)), precision));  // 计算股
            // 通过勾股定理求斜边长(勾三股四弦五),斜边长也是新正n边形的边长
            edgeLength=sqrt(gu.pow(2).add(gou.pow(2)), precision);
            // 正n边形的周长除以直径,即可得到近似的圆周率数值
            π=edgeLength.multiply(edgeNumber).divide(d, mc);
        }
        return π;
    }

然后外部调用新改造的getPaiExact方法,具体的调用代码示例如下:

    // 利用大小数计算圆周率
    private static void calculateByBigDecimal() {
        int n=165;  // 大数字割到第165次,求得的圆周率可精确到小数点后100位
        BigInteger edgeNumber=BigInteger.valueOf(2).pow(n). multiply(BigInteger.valueOf(6));
        System.out.println("割圆次数=" + n + ", 内接正N边形的边数=" + edgeNumber);
        // 割圆术内接到正280608314367533360295107487881526339773939048251392边形(n=165)
        // 计算出来的圆周率精确到小数点后100位
        BigDecimal π_exact=getPaiExact(n);
        System.out.println("精确的圆周率数值=" + π_exact);
    }

运行上面的圆周率求解代码,观察到下面的输出日志:

割圆次数=165,内接正N边形的边数=280608314367533360295107487881526339773939048251392

精确的圆周率数值=3.1415926535897932384626433832795028841971693993751058209749445923078164062 862089986280348253421170679165188670

由日志可见,利用大整数类型成功求得了内接正N边形的边数,同时利用大小数类型也成功将圆周率数值推导至小数点后面100位。