快速浮点开方运算

Posted yutianzuijin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快速浮点开方运算相关的知识,希望对你有一定的参考价值。

代码下载:开根号的几种算法实现
在之前的博客中我们介绍了数据类型的地址转换,利用它我们可以将一个float型的值直接看成一个int类型。这种地址转换到底有什么意义,或者说有什么用途呢?今天,给大家展示一个实例—快速浮点开方运算,让大家更加明白地址转换的含义和它们之间的对应关系。

1 二分法

浮点开方也就是给定一个浮点数x,求 x \\sqrt x x 。这个简单的问题有很多解,我们从最简单最容易想到的二分开始讲起。利用二分进行开平方的思想很简单,就是假定中值为最终解。假定下限为0,上限为x,然后求中值;然后比较中值的平方和x的大小,并根据大小修改下限或者上限;重新计算中值,开始新的循环,直到前后两次中值的距离小于给定的精度为止。**需要注意的一点是,如果x小于1,我们需要将上限置为1,原因你懂的。**代码如下:

float SqrtByBisection(float n)

	float low,up,mid,last; 
	low=0,up=(n<1?1:n); 
	mid=(low+up)/2; 
	do
	
		if(mid*mid>n)
			up=mid; 
		else 
			low=mid;
		last=mid;
		mid=(up+low)/2; 
	while(fabsf(mid-last) > eps);

	return mid; 

这种方法非常直观,也是面试过程中经常会问到的问题,不过这里有一点需要特别注意:在精度判别时不能利用上下限而要利用前后两次mid值,否则可能会陷入死循环!这是因为由于精度问题,在循环过程中可能会产生mid值和up或low中的一个相同。这种情况下,后面的计算都不会再改变mid值,因而在达不到精度内时就陷入死循环。但是改为判断前后两次mid值就不会有任何问题(为啥自己想)。大家可以找一些例子试一下,这可以算是二分法中的一个trick。二分虽然简单,但是却有一个非常大的问题:收敛太慢!也即需要循环很多次才能达到精度要求。这也比较容易理解,因为往往需要迭代3到4次才能获得一位准确结果。为了能提升收敛速度,我们需要采用其它的方法。

2 牛顿迭代法

原理也比较简单,就是将中值替换为切线方程的零根作为最终解。原理可以利用下图解释(from matrix67):

图一 牛顿迭代法求开方
假设现在要求 a \\small \\sqrta a 的值(图中a=2),我们将其等价转化为求函数 y = x 2 − a \\small \\small y=x^2-a y=x2a与x轴大于0的交点。为了获得该交点的值,我们先假设一个初始值,在图一中为 x 0 = 4 \\small \\small x_0=4 x0=4。过 x = x 0 \\small \\small x=x_0 x=x0的直线与 y = x 2 − 2 \\small \\small y=x^2-2 y=x22交于一点 ( x 0 , y 0 ) \\small \\small \\left ( x_0,y_0 \\right ) (x0,y0),过该点做切线交x轴于 x 1 \\small \\small x_1 x1,则 x 1 \\small \\small x_1 x1是比 x 0 \\small x_0 x0好的一个结果。重复上述步骤,过 x = x 1 \\small \\small x=x_1 x=x1直线与 y = x 2 − 2 \\small \\small y=x^2-2 y=x22交于一点 ( x 1 , y 1 ) \\small \\small \\left (x_1,y_1 \\right ) (x1,y1),过该点做切线交x轴于 x 2 \\small \\small x_2 x2,则 x 2 \\small \\small x_2 x2是比 x 1 \\small \\small x_1 x1更好的一个结果……
很明显可以看出该方法斜着逼近目标值,收敛速度应该快于二分法。但是如何由 x 0 \\small x_0 x0获得 x 1 \\small \\small x_1 x1呢,我们需要获得一个递推公式。看图中的阴影三角形,竖边的长度为 y 0 \\small \\small y_0 y0,如果我们能求得横边的长度l,则很容易得到 x 1 = x 0 − l \\small \\small x_1=x_0-l x1=x0l。因为三角形的斜边其实是过 ( x 0 , y 0 ) \\small \\small \\left ( x_0,y_0 \\right ) (x0,y0)的切线,所以我们可以很容易知道该切线的斜率为 f ′ ( x 0 ) \\small \\small f&#x27;(x_0) f(x0),然后利用正切的定义就可以获得l的长度为 y 0 f ′ ( x 0 ) \\small \\small \\fracy_0f&#x27;(x_0) f(x0)y0,由此我们得到递推公式为:
x i = x i − 1 − y 0 f ′ ( x 0 ) = x i − 1 − x i − 1 2 − a 2 ∗ x i − 1 = 1 2 ( x i − 1 + a x i − 1 ) \\small \\small x_i=x_i-1-\\fracy_0f&#x27;(x_0)=x_i-1-\\fracx_i-1^2-a2*x_i-1=\\frac12\\left ( x_i-1+\\fracax_i-1 \\right ) xi=xi1f(x0)y0=xi12xi1xi12a=21(xi1+xi1a)
后面我们需要做的就是利用上面的公式去迭代,直到达到精度要求,代码如下:

float SqrtByNewton(float x)

	float val=x;//初始值
	float last;
	do
	
		last = val;
		val =(val + x/val) / 2;
	while(fabsf(val-last) > eps);
	return val;

对上述代码进行测试,结果确实比二分法快,对前300万的所有整数进行开方的时间分别为1600毫秒和1000毫秒,快的原因主要是迭代次数比二分法更少。虽然牛顿迭代更快,但是还有进一步优化的余地:首先牛顿迭代的代码中有两次除法,而二分法中只有一次,通常除法要比乘法慢个几倍,因而会导致单次迭代速度的下降,如果能消除除法,速度还能提高不少,后面会介绍没有除法的算法;其次我们选择原始值作为初始估值,这其实不是一个好的估计,这就导致需要迭代多次才能达到精度要求。当然二分法也存在这个问题,但是上下限不容易估计,只能采用最保守的方式。而牛顿迭代则可以任意选择初始值,所以就存在选择的问题。
我们分析一下为什么牛顿迭代法可以任意选择初值(当然必须要大于0)。由公式
x i = 1 2 ( x i − 1 + a x i − 1 ) \\small \\small x_i=\\frac12\\left ( x_i-1+\\fracax_i-1 \\right ) xi=21以上是关于快速浮点开方运算的主要内容,如果未能解决你的问题,请参考以下文章

python中如何进行开方运算

C# 平方开方保留小数 运算

为啥 roundf() 不对浮点值进行舍入,为啥 int - float 数学运算返回错误值?

PHP的开方运算

算数运算符注意事项

为啥for循环最少避免使用浮点型变量作为循环变量