目录
第一步:❓我们要解决什么?
第二步:将其类比为求自然数和
第三步:什么是每一项?
第四步:定义要计算的每一项(term)
第五步:定义递归函数结构
🌳 调用树结构
完整代码
复杂度分析
🔚 总结图(从定义到实现):
我们将用第一性原理来一步一步推导如何用 C++ 实现泰勒展开式(Taylor Series Expansion),并借助递归求自然数之和的思想进行类比和迁移。
第一步:❓我们要解决什么?
我们要用 C++ 写一个函数,计算 e^x 的近似值,基于泰勒展开公式。
第二步:将其类比为求自然数和
你已经知道:
int sumOfNumbers(int n)
{if (n == 0)return 0;elsereturn sumOfNumbers(n - 1) + n;
}
我们同样可以将泰勒级数看作是:
f(x) = 第0项 + 第1项 + 第2项 + … + 第n项
所以我们可以尝试用递归方法去表达每一项,累加所有项。
第三步:什么是每一项?
我们先思考“泰勒展开”的一项的数学结构:
我们从第一性原理推导出这个公式的两部分组成:
-
分子是 x^n,也就是 x * x * x……(共n次)
-
分母是
n!
=n * (n−1) * (n−2)⋯1
所以我们必须做两件事:
-
每次递归都乘一次 x → 累计分子
-
每次递归都乘一次当前 n → 累计分母
从递归角度重新理解:
如果你在写一个 taylor(n)
递归函数,你会每次调用去表示:
-
我要计算第 n 项,
-
第 n 项是第 n−1 项 * x / n
于是我们可以这样递推:
✅ 这个推导说明:
我们不需要单独重复计算 x^n 和 n
我们可以利用上一步的结果,乘一个新的因子 x / n 就能得到下一项。
第四步:定义要计算的每一项(term)
❓问题来了:
如果我们想用递归函数一步一步计算:
double taylor(int n, double x);
那么问题是:
-
上一项计算的结果在哪里?
-
本层计算需要的 x^(n-1) 和 (
n-1)!
怎么得来?
直觉尝试:用返回值传信息?
你可能会尝试每次都重新计算 x^n 和 n!
,像这样:
double taylor(int n, double x) {return taylor(n-1, x) + pow(x, n) / factorial(n); // NOT efficient
}
问题:
-
每一层都重复算一遍幂和阶乘
-
没有重用信息
✅ 真正的优化思路:我们需要“带状态的递归”
我们希望每次调用都记住前一项计算中积累出来的 x^n
和 n!
于是我们可以:
-
在函数内部保留静态变量(或全局变量)
-
让它在每一层递归中不断更新
我们引入三个全局变量:
double num = 1; // 分子(x^n)
double den = 1; // 分母(n!)
每次递归做的事情就是:
-
分子(幂函数)乘上 x
-
分母(阶乘)乘上 n
-
计算这一项 term = num / den
-
递归进入下一层(直到 n == 0 为止)
为什么不用局部变量?
-
每一层递归函数自己的局部变量在返回时会销毁,状态无法累积。
-
静态/全局变量可以在整个递归调用链中持续保存状态。
第五步:定义递归函数结构
我们遵循:
-
先处理“底部终止条件”(n == 0)
-
然后递归地构造前一项的和
-
最后处理当前这一项的贡献
否则:你将会提早计算当前项(还没到你这一层),状态错位。
Step 1:递归函数要怎么“停下来”?
在定义任何递归函数的时候,必须先回答一个问题:
✨ 什么时候这个函数应该“终止”,不再递归?
这就是我们要设定的base case(基础情况)。
在泰勒展开中,第 0 项是已知的常数 1,所以我们在 n=0 时应该返回:1
if (n == 0) return 1;
✅ 这是递归的“锚点”,你必须先写,否则递归会无限下去。
Step 2:递归要先“构造前面的和”
✨思维实验:
假设你现在写的是:
double taylor(int n, double x) {num *= x;den *= n;double res = taylor(n - 1, x);return res + num / den;
}
你发现问题了没?
你先更新了 num 和 den,然后才去计算上一项的结果,这就打乱了状态的对应关系——因为上一
项本来是 x^(n-1) / (x-1) !,但你已经把它提前变成 x^n / x ! 了。
错误:我们在进入上一个递归之前,就已经变更了状态!
✅ 正确方式:
我们应该:
-
先去计算 上一项的累加和(taylor(n - 1))
-
回来以后再更新状态
-
然后把当前这一项加进去
double taylor(int n, double x) {if (n == 0) return 1;double res = taylor(n - 1, x); // 🚶先去处理前面项num *= x; // ⏪回来再乘 xden *= n; // ⏪回来再乘 nreturn res + num / den; // ⏫最后加上当前这一项
}
注意!这是典型的“递归先深入到底部(递归树的叶子)”,然后在回溯的过程中逐层累计每一项。
🌳 调用树结构
示例:调用 taylor(3, x)
,即展开 4 项 (n=3)
我们每调用一次函数,都画出一层,并在返回时说明计算了哪一项。
taylor(3)↓taylor(2)↓taylor(1)↓taylor(0) ← base case = 1
然后回溯计算(从下向上):
-
返回 taylor(0) = 1
-
回到 taylor(1):
-
分子 num *= x → x
-
分母 den *= 1 → 1
-
加项 x^1/1! = x
-
-
回到 taylor(2):
-
num *= x → x²
-
den *= 2 → 2
-
加项 x^2 / 2!
-
-
回到 taylor(3):
-
num *= x → x³
-
den *= 3 → 6
-
加项 x^3 / 3!
-
taylor(0) = 1
taylor(1) = taylor(0) + x / 1
taylor(2) = taylor(1) + x^2 / 2
taylor(3) = taylor(2) + x^3 / 6
回溯顺序:
taylor(1) ← + x^1 / 1! ← 分子: x,分母: 1
taylor(2) ← + x^2 / 2! ← 分子: x²,分母: 2
taylor(3) ← + x^3 / 3! ← 分子: x³,分母: 6
你看到没,正是因为我们在回溯阶段才处理当前项,才确保每一项都在它该在的位置!
调用层 | n | 分子(num) | 分母(den) | 当前项 |
---|---|---|---|---|
3 | 3 | x^3 | 3 !=6 | x^3 / 6 |
2 | 2 | x^2 | 2 !=2 | x^2 / 2 |
1 | 1 | x | 1 !=1 | x/1 |
0 | 0 | 1(初始值) | 1(初始值) | 1 |
完整代码
double num = 1; // 分子
double den = 1; // 分母double taylor(int n, double x) {if (n == 0) return 1; // 1️⃣ 锚点:停止递归double res = taylor(n - 1, x); // 2️⃣ 先构造前面所有项的和num *= x; // 3️⃣ 然后再更新状态den *= n;return res + num / den; // 4️⃣ 当前项加进去
}
复杂度分析
这是一个非常经典的线性递归(linear recursion)结构。我们来详细分析其时间复杂度和空间复杂度。
⏱️时间复杂度分析(Time Complexity)
我们从函数调用过程来看:
-
调用
taylor(n)
会递归调用taylor(n-1)
-
taylor(n-1)
又会调用taylor(n-2)
-
...
-
最后到底部
taylor(0)
返回 1,然后逐层回溯
整个递归链条是:
taylor(n)→ taylor(n-1)→ taylor(n-2)...→ taylor(0)
总共会调用 n+1
次函数(从 0 到 n)。
每一层递归执行的是:
-
一个函数调用(
taylor(n - 1)
) -
两次乘法:
num *= x
,den *= n
-
一次加法:
res + num / den
这些操作都是常数时间 O(1)。
✅ 所以:时间复杂度为:O(n)
空间复杂度分析(Space Complexity)
这就有趣了,因为这是递归。
你没有使用堆内存(比如数组、链表),但递归函数本身会在调用栈上分配帧:
-
每调用一次
taylor(n)
,系统会开辟一块栈帧来保存:-
参数
n
,x
-
局部变量
res
-
返回地址
-
由于这个是线性递归(不是尾递归),函数不会在递归过程中被优化掉(除非特别启用尾递归优化,C++ 一般不保证)。
✅ 所以:空间复杂度为:O(n)
🔚 总结图(从定义到实现):
数学定义:term_n = x^n / n! ← 每一项都能用前一项 * (x/n) 得到递归目标:taylor(n) = taylor(n-1) + term_n中间状态:num ← num * xden ← den * n于是代码:num = 1;den = 1;double taylor(n, x) {if n==0 return 1;res = taylor(n-1, x);num *= x; den *= n;return res + num / den;}