当前位置: 首页 > 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;}

http://www.lqws.cn/news/161821.html

相关文章:

  • SAP学习笔记 - 开发24 - 前端Fiori开发 Filtering(过滤器),Sorting and Grouping(排序和分组)
  • Docker MCP 目录和工具包简介:使用 MCP 为 AI 代理提供支持的简单安全方法
  • 阿里云服务器安装nginx并配置前端资源路径(前后端部署到一台服务器并成功访问)
  • Spring Boot 使用 SLF4J 实现控制台输出与分类日志文件管理
  • uv管理spaCy语言模型
  • 使用Hutool工具进行rsa加密解密示例:
  • JVM垃圾回收器-ZGC
  • GC1809:高性能音频接收与转换芯片
  • NineData云原生智能数据管理平台新功能发布|2025年5月版
  • SpringCloud——Nacos
  • SDC命令详解:使用set_fanout_load命令进行约束
  • 可穿戴设备:健康监测的未来之眼
  • clickhouse常用语句汇总——持续更新中
  • 牛客小白月赛113
  • Git的由来与应用详解:从Linux内核到现代开发的革命性工具
  • windows server2019 不成功的部署docker经历
  • [特殊字符] 一文了解目前主流的 Cursor AI 免费续杯工具!
  • AI时代的弯道超车之第二十四章:AI伦理和版权问题
  • 智慧园区数字孪生全链交付方案:降本增效30%,多案例实践驱动全周期交付
  • STM32入门教程——OLED调试工具
  • Elasticsearch最新入门教程
  • vue3 eslint ts 关闭多单词命名检查
  • AirSim/Cosys-AirSim 游戏开发(二)使用自定义场景
  • 大模型学习
  • adb 连不上真机设备问题汇总
  • uniapp微信小程序视频实时流+pc端预览方案
  • 音视频之视频压缩编码的基本原理
  • Rust Floem UI 框架使用简介
  • 从《现实不似你所见》探寻与缘起性空的思想交织
  • OPenCV CUDA模块目标检测----- HOG 特征提取和目标检测类cv::cuda::HOG