当前位置: 首页 > news >正文

数据结构:递归:泰勒展开式(Taylor Series Expansion)

目录

第一步:❓我们要解决什么?

第二步:将其类比为求自然数和

第三步:什么是每一项?

第四步:定义要计算的每一项(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^nn!

于是我们可以:

  • 在函数内部保留静态变量(或全局变量)

  • 让它在每一层递归中不断更新

我们引入三个全局变量:

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 !​ 了。

错误:我们在进入上一个递归之前,就已经变更了状态!

✅ 正确方式:

我们应该:

  1. 先去计算 上一项的累加和(taylor(n - 1))

  2. 回来以后再更新状态

  3. 然后把当前这一项加进去

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)当前项 
33x^33 !=6x^3 / 6
22x^22 !=2x^2 / 2
11x1 !=1x/1
001(初始值)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;}

相关文章:

  • 架构师级考验!飞算 JavaAI 炫技赛:AI 辅助编程解决老项目难题
  • 单精度浮点数值 和 双精度浮点数值
  • 嵌入式学习之系统编程(十)网络编程之TCP传输控制协议
  • TDengine 开发指南—— UDF函数
  • Web 架构相关文章目录(持续更新中)
  • YAML在自动化测试中的三大核心作用
  • RADIUS-管理员获取共享密钥
  • 拆装与维修汇总帖
  • Qt/C++学习系列之QGroupBox控件的简单使用
  • Linux项目自动化构建工具——make/Makefile
  • 掌握YOLOv8:从视频目标检测到划定区域统计计数的实用指南
  • 6.824 lab1
  • float、double 这类 浮点数 相比,DECIMAL 是另一种完全不同的数值类型
  • 动态表单 LiveCycle 与 AcroForms 对比
  • 东南亚用工合规困境破解:从文化冲突到数字化管理升级
  • 央国企人力资源数字化转型:全景路径与6类挑战解析
  • vite+vue3项目中,单个组件中使用 @use报错
  • 群晖NAS套件历史版本资源
  • 动态规划---股票问题
  • AiPy实战:10分钟用AI造了个音乐游戏!
  • 网站美工设计收费/正规的培训机构有哪些
  • 网站建设 翰臣科技/企业网站注册域名的步骤
  • 如何做网站关键字优化/营销必备十大软件
  • 毕业设计网站可以做什么/成都seo优化排名推广
  • 网站建设的一些销售技巧/软件外包公司排名
  • 做saas网站可行吗/天津疫情最新情况