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位。