为什么显示nan?

3

好的,我正在进行一个程序,我试图使右侧的结果与左侧相等,精确到0.0001%
正弦x = x - (x ^ 3) / 3!+(x ^ 5)/ 5!+(x ^ 7)/ 7!+ ....

#include<iostream>
#include<iomanip>
#include<math.h>

using namespace std;

long int fact(long int n)
{
    if(n == 1 || n == 0)
        return 1;
    else
        return n*fact(n-1);
}

int main()
{
    int n = 1, counts=0; //for sin
    cout << "Enter value for sin" << endl;
    long double x,value,next = 0,accuracy = 0.0001;
    cin >> x;
    value = sin(x);
    do
    {
        if(counts%2 == 0)
            next = next + (pow(x,n)/fact(n));
        else
            next = next - (pow(x,n)/fact(n));
        counts++;
        n = n+2;
    } while((fabs(next - value))> 0);
    cout << "The value of sin " << x << " is " << next << endl;
}


假设我输入了 x 的值为 45 我得到的结果是
sin 45 的值为 NaN(非数值)。


有没有人能帮我看看我哪里错了?


45! 约为 1e56。这个数字超出了 long 的范围,因此您的 fact 函数无法返回正确的答案。 - Alan Stokes
2
你尝试过打印出每个next的连续值吗?这样或许能更容易看出问题出在哪里。 - Alan Stokes
你是否意识到你的方程式(以及std::sin()函数本身)在x以弧度表示时给出x的正弦值,而不是角度?此外,当要求你编写自己的sin()函数时,通常假定不存在其他sin()函数。 - Klitos Kyriacou
4个回答

2
首先,您的while条件应该是while((fabs(next-value))> accuracy),并且fact应该返回long double类型。即使更改了这些内容,对于45的值仍然无法正常工作。原因是这个泰勒级数收敛速度太慢了,对于大的值来说,误差太大了。这里是公式中的误差项:

enter image description here

其中k是迭代次数,a=0,函数为sin。为了使条件变为false,45^(k+1)/(k+1)!次一定要有sincos(取决于第k阶导数)的绝对值(在0和1之间)小于0.0001。对于50的值,在这个公式中,这个数字仍然非常大(我们应该期望误差约为1.3*10^18,这意味着我们肯定会进行超过50次迭代)。45^5050!将溢出,然后将它们除以会得到infinity/infinity=NAN。在您的原始版本中,fact值不适合于整数(您的值溢出为0),然后除以0会给您无穷大,再减去另一个无穷大会给您NAN

我认为fact溢出了,没错。但是我不明白为什么*运算符对于long double会返回NaN。我认为这是来自pow函数。可能是NaN / UB(未定义行为)。 - Ely
@Elyasin 两者都会变成无穷大,这就是NAN的来源。阶乘增长比幂函数更快。 - Abstraction
原始的 fact 会导致 * 溢出为零,因此 x +y/0nan。如果您修复了 fact ,它将溢出为 inf 并在后面产生 nan - JSF
我不确定我是否理解。在程序中,x45 开始,n = 045^i 应该比 i! 增长更快,不是吗? - Ely
@JSF 是的,你说得对,在他的原始版本中(返回整数的事实),除以零会导致inf,然后从另一个inf中减去会导致“NAN”。 - Abstraction
即使您修复了 (pow(x,n)/fact(n)) 的计算,使其永远不会溢出,结果仍然永远不会收敛。 当您在这些数很大时计算其中两个之间的差异时,会产生舍入误差。 这种舍入误差从总和中永远不会被消除,因此答案永远无法接近。 - JSF

0

如果选择了45,则意味着您认为输入是角度制,您应该重新考虑并可能应该将其缩小模2π。

首先修复两个错误:

    long double fact(long int n)
...
      }while((fabs(next - value))> accuracy);

如果使用long int,则fact的返回值会很快溢出。即使使用long doublefact的返回值最终也会溢出。当你将比较对象从accuracy改为0时,答案永远不够正确,因此只有nan可以停止while循环。

由于舍入误差,你仍然无法收敛(当pow提供的值大于fact时,你正在计算大数之间的差异,这会积累显著的舍入误差,而这些误差永远不会被消除)。因此,你可以在每个循环步骤中增加n之前,先计算long double m=pow(x,n)/fact(n);,然后使用:

}while(m > accuracy*.5);

此时,要么答案已经达到了指定的精度,要么剩余误差被舍入误差所主导,进一步迭代也无济于事。


0

我引用这里关于pow的内容:

返回值

如果没有错误发生,则返回底数base的指数exp(或iexp)(baseexp)。

如果发生域错误,则返回实现定义的值(在支持的情况下为NaN

如果发生极点错误或由于溢出而导致范围错误,则返回±HUGE_VAL、±HUGE_VALF或±HUGE_VALL。

如果由于下溢而导致范围错误,则返回正确的结果(经过舍入后)。

继续阅读:

错误处理

...

除非上述已指定,否则如果任何参数为NaN,则返回NaN

所以基本上,由于n正在增加并且您有许多循环,pow返回NaN(您使用的编译器显然支持该功能)。其余部分是算术运算。您使用溢出值进行计算。

我相信你正在尝试使用泰勒级数来近似计算sin(x)。我不确定这是否是正确的方法。

也许你可以尝试在循环中一旦遇到NaN就停止,并且不更新变量next,直接输出结果。我认为这是你的算法能够得到的最接近的结果。


你为什么认为 pow 会溢出,而不是 fact 先溢出呢? - JSF
fact 不会返回 NaN,是 pow 函数造成的。 - Ely
如果不进行纠正,fact会溢出为0,如果进行纠正,fact会溢出为infnan是由于0或inf导致的。 - JSF
无论如何,我明白你的意思。我认为那个程序代码中的 pow 是第一个 NaN 罪犯。实际上这就是我想说的。但你是对的,无论哪种方式都是一个问题... - Ely
当我使用45作为输入进行测试时(即使修复了fact),fact首先溢出。 - JSF
我向你发起挑战,分享你的测试程序。 - Ely

0

如果您使用了合理级别的警告编译系统,您会立即发现您没有使用变量accuracy。这和您的fact函数返回一个long int只是您问题的一小部分。即使您纠正了这些问题,您也永远不会得到sin(45)的好结果。

问题在于,对于x=45sin(x)的泰勒展开式中的项直到n=45才开始减少。这是一个大问题,因为4545/45!是一个非常大的数字,2428380447472097974305091567498407675884664058685302734375 / 1171023117375434566685446533210657783808,或大约2*1018。您的算法最初添加和减去巨大的数字,只有在20多次加法/减法之后才开始减少,最终希望结果在-1和+1之间。鉴于输入值为45且使用本机浮点类型,这是一个无法实现的希望。

你可以在算法中使用一些BigNum类型(网络上有很多),但是当你只需要四位精度时,这是极端的过度。或者,你可以利用sin(x)的周期性质,sin(x+2*pi)=sin(x)。输入值45等同于1.017702849742894661522992634...(模2*pi)。对于输入值1.017702849742894661522992634,你的算法效果非常好。

你可以做得比那更好,通过将输入值取模2*pi,这是计算正弦和余弦的合理算法的第一步。更好的是,你可以利用sin(x+pi)=-sin(x)的事实。这使你将范围从负无穷到正无穷缩小到0到pi。甚至更好的是,在0到pi之间,sin(x)关于pi/2对称。你可以做得比这更好。三角函数的实现充分利用了这些行为,但它们通常不使用泰勒逼近。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接