在数学中,从整数取幂到实数取幂的步骤是相当大的一步。虽然整数求幂可以定义为重复乘法(例如x^5 = x*x*x*x*x
) 指数中实数值的求幂通常是通过自然指数函数定义的。你的例子2^0.5
可以重写为
2^0.5 = exp(0.5*ln(2))
where exp
是自然指数函数并且ln
是它的倒数,即自然对数。这两个函数都具有众所周知的幂级数:
exp(x) = 1 + x + x^2/2 + x^3/3 + ...
ln(x) = (x-1) - (x-1)^2/2 + (x-1)^3/3 - ...
幂级数是无穷级数,因此您无法精确计算它,但根据您希望结果的精确程度,您可以在级数中的某个点停止。整个系列的公式可以在以下位置找到:维基百科 https://en.wikipedia.org/wiki/Taylor_series#Exponential_function.
请注意,对数的级数展开仅对 0 到 2 之间的 x 值收敛。在您的示例中,ln(2)
已经到了这个区域的边缘。通过进一步重写表达式可以找到此问题的解决方案。对于任何数字x
, ln(x)
可以扩展为
ln(x) = ln(m*2^e) = ln(m) + e*ln(2)
Where m
是尾数(1 到 2 之间)并且e
是数字 x 的指数。在您的示例中,您可以这样写:
ln(2) = ln(1*2^1) = ln(1) + 1*ln(2)
的价值ln(2)
可以预先计算并存储为常数,因为它总是相同的。的价值ln(1)
可以使用上面的级数展开来准确计算(在您的示例中,这恰好是ln(1) = 0
,但一般情况并非如此)。尾数和指数可以很容易地从double
or float
类型,通过使用其内部位表示 https://en.wikipedia.org/wiki/Single-precision_floating-point_format.
有了这些信息,我们就可以尝试编写自己的ln
, exp
最终pow
功能:
#include <stdio.h>
/* constant to control how many terms in the series expansion we want to calculate before aborting */
const int N = 50;
float my_ln(float x) {
const float ln2 = 0.6931471805599453;
union {
float f;
uint32_t i;
} u;
/* get the mantissa by bit manipulation of the float binary representation */
u.f = x;
u.i = (u.i & 0x07ffffff) | 0x3f800000;
/* calculate the logarithm according to its series expansion */
float result = 0.0;
float tmp = u.f - 1.0;
for ( int i = 1; i < N; i++ ) {
result += tmp/i;
tmp *= 1.0 - u.f;
}
/* get the exponent */
u.f = x;
u.i = ((u.i >> 23) & 0xff) - 127;
/* multiply it with ln(2) and add it to the logarithm of the mantissa */
result += (float)u.i * ln2;
return result;
}
float my_exp(float x) {
float result = 0.0;
float tmp = 1.0;
for ( int i = 1; i < N; i++ ) {
result += tmp;
tmp *= x/i;
}
return result;
}
float my_pow(float x, float a) {
/* multiply the logarithm of the base x with the exponent a and compute the natural exponential of this value */
return my_exp( my_ln(x) * a);
}
int main() {
float x = 2.0;
float a = 0.5;
printf("%f ^ %f == %f\n", x,a, my_pow(x,a));
}
这给出了相当好的结果,至少对于我测试的几个例子来说是这样。请注意,编写此类函数很困难且容易出错。例如,我上面提供的函数不能正确处理负数。 NaN、无穷大和零也没有得到正确处理。虽然这也可以通过更多的努力来完成,但人们确实应该使用标准库中的函数,因为它们是
- 经过充分测试
- 可能更快并且经过优化
- 在更极端的数字区域(非常小或很大的数字)可能更准确