定义函数:
float powf(float x, float y);
要求计算:
z=xy(1)注意这里的两个数都是浮点数,因此不可能通过循环相乘的办法得到,可以使用其它办法,把上面的公式两边取对数:
log2z=log2xy=y×log2x(2)因此只要先求出log2x然后再乘以y,然后将得到的值再作为2的指数进行运算即可。也就是说:
z=2y×log2x这里有两个问题:
- 如何去求这个log2x?
- y×log2x有可能不是一个整数,那么将其作为2的指数又该怎么去计算?
求解log2x:#
那么第一个的问题就在于怎么去计算这个log2x,我们可以使用泰勒公式,将其展开,但是注意,这并不是一个经典的泰勒公式的问题,我们需要进行推导。
直接求log2x并不好求,我们可以利用lnx来求。
我们有如下的泰勒公式:
ln(1+x)=x−21x2+31x3−41x4+…=k=1∑nk(−1)kxk(3)上述公式的理论范围为x∈R,x∈[−1,+∞),但是实际上考虑到上述公式的推导过程,这个公式只有在x在0附近才会有一个比较好的精度,因此通常限定x的范围为x∈[−1,1],另外注意,这里是ln(1+x),我们想要求的是lnx,所以通常的做法是:
let t=x−1,then x=t+1,thereforelnx=ln(t+1)=k=1∑nk(−1)ktk(4)此时x在x∈(0,2)时有比较好的精度。但是如果x过大的话,就会有非常大的精度下降的问题。
除此之外,我们还有另外一个公式:
lnx=2[(x+1x−1)+31(x+1x−1)3+51(x+1x−1)5+…]=2k=0∑n2k+11(x+1x−1)2k+1(5)这个公式的推导方法如下:
ln(1+x)=k=1∑nk(−1)kxk when x∈[−1,+∞)(6)将其中的x代换成x1可以得到:
ln(1+x1)=ln(xx+1)=k=1∑nk(−1)kxk1(7)而x的区间范围为:
x1∈(−1,+∞)⟹x∈(−∞,−1)∪(0,+∞)(8)我们将(6)中的x再度替换:
ln(1−x1)=−k=1∑nkxk1(9)然后对(9)的两边做处理:
⇒ln(1−x1)=−k=1∑nkxk1−ln(1−x1)=−ln(xx−1)=lnx−1x=k=1∑nkxk1(10)照例分析一下x的区间范围,不难得出:
−x1∈(−1,+∞)⟹x∈(−∞,0)∪(1,∞)(11)然后把(7)式和(10)式相加,得到:
ln(xx+1)+ln(x−1x)=ln(x−1x+1)k=1∑nkxk(−1)k+k=1∑nkxk1=2k=0∑n2k+11x2k+11因而:
ln(x−1x+1)=2k=1∑n2k+11x2k+11(12)再将(9)和(11)对于x的分析进行一个合并,不难得出:
x∈(−∞,−1)∪(1,∞) or ∣x∣>1(13)然后,我们做如下的处理:
let x−1x+1=z这里分析一下这个z的值域。当x>1时,我们有:
x→1limx−1x+1x→+∞limx−1x+1=+∞=1上面的两式告诉我们两点:
1. x−1x+1 在x∈(1,+∞)上连续。2. x−1x+1∈(1,+∞) 当x∈(1,+∞)时。(14)再来看x≤−1的情形:
1. x−1x+1x=−1=02. x→−∞limx−1x+1=1因而:
1. x−1x+1 在x∈(−∞,−1)上连续。2. x−1x+1∈(0,1) 当x∈(−∞,−1)时。(15)结合(14)和(15)的结论,我们得出:
x−1x+1=z∈(0,+∞)(16)于是我们得出我们最终的结论:
lnx=2[(x+1x−1)+31(x+1x−1)3+51(x+1x−1)5+…]=2k=0∑n2k+11(x+1x−1)2k+1(x∈R and x>0)(17)为什么(17)式比(4)式要更好:
理论上来说,只要展开到无穷项,就可以获得相同的精度,但问题在于我们通常不可能真的将其展开到无穷项,而只是展开到有限项求一个近似。(4)式的问题在于,假定我们只展开到第k−1项,那么它的余项为:
o(k(−1)ktk) where t=x−1这个余项,实际上就是误差项,它的问题在于,如果x≥2,那么t>1,这会使得这最后一项产生非常严重的发散问题,从而造成极大的误差,因此公式(3)只有在x∈(0,2)时才会有比较好的精度。
再来看式(17)我们发现:
x≥0,x+1x−1x=0=−1,x→+∞limx+1x−1=1,x+1x−1∈[−1,1]假定展开到第k项,那么它的余项是:
o(2k+11(x+1x−1)2k+1)这一项可以保证始终在一个小区间内波动,此外它的单调性与lnx也趋于一致,因此式(17)有更好的收敛性,从而在接收较大一点的数时仍然可以保持比较高的精度。不过因为通常展开的项数不够多,当接收的x过大时,其精度也会有相当程度的降低。
但是,既然已经知道这个公式在x越靠近0的时候有较高的精度,那么我们可以这么做:
把x放缩到[1,2]区间内。假定放缩系数为1/2n。
利用公式计算ln(x/2n),此时可以获得比较高的精度。
考虑到ln(x/2n)=ln(x)−nln(2),因此不难得出:
ln(x)=ln2nx+nln(2)(18)而这里的放缩系数实际上很好求,考虑到IEEE754下的浮点数标准,这里的n实际就可以是浮点数x的阶码,而x/2n,实际上相当于把x的阶码替换为127(如果x是float
),或者1023(如果x是double
)。
另外,有关对数函数的计算,笔记中有价值的文章是:((20220708110318-f0uqjrt ‘计算机是怎样计算log的’))。
最终计算xy#
现在我们已经可以计算lnx,那么要求xy就很简单了:
⟹⟹⟹⟹z=xylog2z=log2xylog2z=ylog2xlog2z=yln2lnxz=2ylnx/ln2算法思路#
考虑到计算机实现的效率问题,需要把整个lnx的计算做一个拆分:
let z=x+1x−1,s=z2假定我们将lnx展开到第15项,那么:
lnx≈2z+32z3+52z5+72z7+92z9+112z11+132z13+152z15=z(2+32z2+52z4+72z6+92z8+112z10+132z12+152z14)=z(2+32s+52s2+72s3+92s4+112s5+132s6+152s7)=z×r其中:
r=2+32s+52s2+72s3+92s4+112s5+132s6+152s7=2+s(32+s(52+s(72+s(92+s(112+s(132+152s))))))考虑到我们最终要求的是log2x因此我们这里直接将1/ln2乘到r上,这样一来就得到了系数:
L1L2L3L4L5L6L7L8=ln22=2.8853900817779268=3ln22=0.9617966939259757=5ln22=0.5770780163555853=7ln22=0.41219858311113244=9ln22=0.3205988979753252=11ln22=0.2623081892525388=13ln22=0.2219530832136867=15ln22=0.19235933878519512因此把上面全部代入,就是:
log2xr′=z×r′=L1+s(L2+s(L3+s(L4+s(L5+s(L6+s(L7+L8s))))))但是如果只是这样的话,当x非常大的时候,精度会非常不够,因此需要做一个放缩,也就是不要直接使用上面的公式,而是将x假定x很大的时候,缩小到1附近。
假定缩小2n后在1附近,那么:
x′log2x′log2x=2nx=z×r′=log22nx′=n+log2x′然后再将log2x与y相乘,再将其作为2的指数即可计算出最终结果:
μz=y×log2x=2μ不过通常来说,这里的μ很可能不是一个整数,那么我们怎么得到它的值?
求2μ的方法#
同样地,恐怕需要利用泰勒公式,我们有如下的泰勒公式:
eμ=1+μ+2!1μ2+3!1μ3+o(4!1μ4)=k=0∑nk!1μk(19)又因为2=eln2,因此我们有:
let t=μln22x=k=0∑nk!1tk(20)但是这里我们再次遇到一个问题,2x的泰勒展式的余项是一个指数形式,是发散的,要获得比较好的精度,上面的需要尽量靠近0才行,但是一般而言,这里的t可能会离0比较远,为了解决这个问题,就需要把得到的t缩小到0附近。但是不要用除法,而是使用减法:
let μ′then μhence 2μ=μ−Δ=μ′+Δ=2μ′+Δ=2μ′×2Δ上面的Δ需要是一个整数,这样在最后计算2μ的时候,就可以使用scalbnf了。而对于2μ′,只需要利用式(20)即可。
这样,最终的xy的值就求出来了。