C++的余弦泰勒级数展开的递归算法

4

我最近参加了一场计算机科学考试,其中要求我们给出cos泰勒级数展开的递归定义。该级数为

cos(x) = 1 - x^2/2! + x^4/4! + x^6/6! ...

函数签名如下:

float cos(int n , float x)

n代表用户想要计算到的数列中的数字,x代表cos函数中x的值。

显然,我没有正确回答这个问题,我已经试图解决它几天了,但是我卡在了一个难题上。

有没有人能够帮助我开始一些地方?


如果你必须使用递归定义,那么将 1 - x^2/2! + x^4/4! - x^6/6! + ... 重写为 1 - (x^2/2)(1 - (x^2/(3*4))(1 - (x^2/(5*6))(1 - ...))) - lurker
我建议您不要使用递归来计算泰勒级数。大多数程序都为递归分配了有限的空间。您可能会耗尽限制,问题是哪个限制。 - Thomas Matthews
@ThomasMatthews 如果他在堆栈上达到了极限,我会非常惊讶;为了获得准确的结果,不需要评估系列的很多成员。 (另一方面,在这里使用递归真的没有必要;一个简单的循环同样好,并且可能更快。) - James Kanze
6个回答

7

到目前为止,所有的答案都是每次重新计算阶乘。我肯定不会这样做。相反,您可以这样写:

float cos(int n, float x)
{
    if (n > MAX)
         return 1;
    return 1 - x*x / ((2 * n - 1) * (2 * n)) * cos(n + 1, x);
}

考虑cos函数返回以下结果(对于点的位置抱歉):
您可以看到这对于n> MAX,n=MAX等都成立。符号交替和x的幂很容易看出。
最后,当n=1时,您得到0!= 1,因此调用cos(1,x)将为您获取cos泰勒展开的前MAX项。
通过开发(当其有少量项时更易于查看),您可以看到第一个公式相当于以下内容:
对于n > 0,在cos(n-1,x)中需要将之前的结果除以(2n-3)(2n-2),并乘以x²。您可以看到当n = MAX + 1时,此公式为1,当n = MAX时,它为1-x²/((2MAX-1)2MAX)等等。
如果允许使用辅助函数,则应更改上述函数的签名为float cos_helper(int n, float x, int MAX),并像下面这样调用它: float cos(int n, float x) { return cos_helper(1, x, n); } 编辑:要将n的含义从已评估项的度数(在此答案中)反转为项数(与问题和以下相同),但仍不会每次重新计算总阶乘,我建议使用两个术语关系。
我们可以简单地定义cos(0,x)=0cos(1,x)=1,并尝试一般实现cos(n,x),即泰勒级数的前n个项之和。
然后对于每个n > 0,我们可以从cos(n-1,x)写出cos(n,x):
cos(n,x) = cos(n-1,x) + x² / (2n)!
接下来对于n > 1,我们尝试使cos(n-1,x)的最后一项出现(因为它是我们要添加的项中最接近的项):
cos(n,x) = cos(n-1,x) + x² / ((2n-1)2n) * ( x^2n-2 / (2n-2)! )
通过将此公式与先前的公式(将其适应于n-1而不是n)相结合:

cos(n,x) = cos(n-1,x) + x² / ((2n-1)2n) * ( cos(n-1,x) - cos(n-2,x) )

我们现在有一个纯递归定义的cos(n,x),没有辅助函数,没有重新计算阶乘,其中n是泰勒展开和中求和项的数量。

然而,我必须强调以下代码的性能非常糟糕:

  • 在性能方面,除非某些优化允许不重新评估在上一步中作为cos( (n-1) - 1, x)评估的cos(n-1,x)
  • 在精度方面,由于抵消效应:我们得到x2n-2 / (2n-2)!的精度非常差

现在这个免责声明已经就位,接下来是代码:

float cos(int n, float x)
{
    if (n < 2)
        return n;
    float c = x * x / (2 * (n - 1) * 2 * n);
    return (1-c) * cos(n-1, x) + c * cos(n-2, x);
}

1
这是正确的方向,但从问题来看,“n代表用户想要计算到的系列中的数字”。 - Cheers and hth. - Alf
@Cheersandhth.-Alf 发现得不错。我更新了答案,考虑到这一点。现在它应该完全按照问题描述的要求执行,但我不会相信它的结果。 - Cimbali
只需为第一种方法编写一个辅助函数(并使用其内容),cos_helper(m,n,x),其中m = MAX,cos(n,x){return cos_helper(n,1,x);} - Lutz Lehmann
如果你可以使用辅助函数,那么你无论如何都可以做任何事情,不是吗?我不确定问题的限制是什么。@KeatonPennels? - Cimbali

1
一个简单的方法是使用静态变量:
double cos(double x, int n) {
    static double p = 1, f = 1;
    double r;

    if(n == 0)
        return 1;

    r = cos(x, n-1);
    p = (p*x)*x;
    f = f*(2*n-1)*2*n;

    if(n%2==0) {
        return r+p/f;
    } else {
        return r-p/f;
    }
}

注意,我在操作中乘以2*n以获得下一个阶乘。 让n与我们需要的阶乘对齐使得这可以在两个操作中轻松完成:f = f * (n - 1) 然后 f = f * n
when n = 1, we need 2!  
when n = 2, we need 4!  
when n = 3, we need 6!  

所以我们可以安全地将n加倍,然后从那里开始工作。我们可以写成:
n = 2*n;
f = f*(n-1);
f = f*n; 如果我们这样做,我们需要更新我们的奇偶检查为if((n/2)%2==0),因为我们正在加倍n的值。
这可以改写为f = f*(2*n-1)*2*n;,现在我们在检查它是偶数还是奇数时不必除以n,因为n没有被改变。

1
cos(x)=1 - x^2/2! + x^4/4! - x^6/6! + x^8/8!.....
=1-x^2/2 (1 - x^2/3*4 + x^4/3*4*5*6 -x^6/3*4*5*6*7*8)
=1 - x^2/2 {1- x^2/3*4 (1- x^2/5*6 + x^4/5*6*7*8)}
=1 - x^2/2 [1- x^2/3*4 {1- x^2/5*6 ( 1- x^2/7*8)}]

    double cos_series_recursion(double x, int n, double r=1){

        if(n>0){

            r=1-((x*x*r)/(n*(n-1)));

            return cos_series_recursion(x,n-2,r);
         }else return r;
    }

我同意静态不是一个好的选择。我现在将其更改为可选参数。 - Atique Reza

0

就像求和一样,把它做了。

cos

float cos(int n, float x)中的参数nl,现在就开始吧... 一些伪代码:

float cos(int n , float x)
{
                //the sum-part
    float sum = pow(-1, n) * (pow(x, 2*n))/faculty(2*n);

    if(n <= /*Some predefined maximum*/)
        return sum + cos(n + 1, x);
    return sum;
}

递归测试不应该是 if ( n >= 0 ) 吗? - James Kanze
“test”是什么意思?中止条件吗?不是的。看看总和,它会增长到无限大,而我们真的负担不起,所以我们会在达到某个指定深度后退出。 - NaCl
这是正确的方向,但从问题来看,“n代表用户想要计算到的系列中的数字”。 - Cheers and hth. - Alf

0
你可以使用循环或递归,但我建议使用循环。无论如何,如果你必须使用递归,你可以使用下面的代码。
#include <iostream>

using namespace std;

int fact(int n) {
    if (n <= 1) return 1;
    else return n*fact(n-1);
}

float Cos(int n, float x) {
    if (n == 0) return 1;
    return Cos(n-1, x) + (n%2 ? -1 : 1) * pow (x, 2*n) / (fact(2*n));
}

int main()
{
   cout << Cos(6, 3.14/6);

}

他不必在阶乘部分使用递归函数,只需使用cos泰勒级数部分。你可以使用循环来计算阶乘,这会更容易理解。 - Félix Cantournet

0

当你想要递归但函数参数不包含所需信息时,通常的技巧是引入一个辅助函数来进行递归。

我有印象在Lisp世界中,惯例是将这样的函数命名为something-aux(缩写为auxiliary),但那可能只是旧时代的一小群人。

无论如何,这里的主要问题是n表示递归的自然结束点,即基本情况,然后您还需要一些索引来工作到n。因此,这是辅助函数的另一个很好的参数候选项。另一个候选项源于考虑系列中的一个术语与前一个术语的关系。


你可以通过倒着计数来实现,但这意味着每次都要重新计算x^(2*n)(2*n)!。否则,你可能需要至少三个额外的变量:一个用于与n比较的计数器,x的幂值的运行值,以及阶乘的运行值。(我可能会为此创建一个类。) - James Kanze
@JamesKanze:我认为,对于尾递归,您只需要运行值。OP没有提到这样的要求。但是我同意,这是最好的方式。 :) - Cheers and hth. - Alf
好观点。我不知道为什么我没有想到它;我过去使用过非尾递归,所以我知道这种可能性的存在。 - James Kanze

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