递归深度问题和尾调用的关系

news2024/9/28 19:10:39

当我们在编写计算阶乘的函数,一般我们都会会选择使用迭代或递归的方法来实现。下面就让我们看看,同一个函数的两种实现方法。首先,是使用迭代方式实现的函数,我们使用循环的方式来计算阶乘:

// 阶乘函数,计算给定正整数 n 的阶乘
const factorial = (n) => {
  // 初始化结果为 1
  let result = 1;
  
  // 使用循环逐步计算阶乘
  while (n > 1) {
    // 乘以当前的 n 值
    result *= n;

    // 减小 n,准备下一次迭代
    n--;
  }

  // 返回最终的阶乘结果
  return result;
}

jcode

接着我们再使用递归的方式来实现同样的函数

// 阶乘函数,递归方式计算给定正整数 n 的阶乘
const factorial = (n) => {
  // 当 n 为 0 时,阶乘为 1
  if (n === 0) {
    return 1;
  }

  // 递归情况:计算 n 与 (n-1) 的阶乘乘积
  return n * factorial(n - 1);
}

jcode

虽然上面这个递归函数和迭代函数的结果是相同的,但浏览器的运行过程中,迭代函数的性能要比递归函数好的多。并且如果我们在递归函数当去计算非常大的数的阶乘时,可能会遇到"RangeError: Maximum call stack size exceeded"错误。这是因为递归函数中的递归调用会在调用栈中积累,当递归深度过深时,调用栈会耗尽系统的内存资源,从而导致错误。

这样说你可能会很懵,那我们就画图来好好理解一下递归是如何工作的。

调用栈

调用栈是一个存储函数调用信息的数据结构。当调用函数时,它会被添加到执行栈中,以及它所调用的所有函数。当一个函数返回时,它会从执行栈中移除。每个添加到栈中的函数称为一个栈帧。

下面我们画出迭代函数和递归函数计算6的阶乘的过程,来更好的理解

迭代函数的替代模型

image.png

递归函数的模型

image.png

通过两张图的对比,我们可以发现。

  • 迭代函数中,我们可以看到每一步的变量状态。并且,在我们的循环的每次迭代中,都会执行一次计算,然后更新存储在内存中的变量。
  • 而在递归函数中,我们不能在执行过程的前半部分看到所有变量的状态。并且,每次执行函数时,都使用更多的内存来存储每次执行的结果值。

你可能会问这会有什么影响呢?

在使用迭代函数计算6的阶乘的过程中,JavaScript将我们的while条件添加到堆栈中,执行计算,然后更新result变量,然后从堆栈中删除while的执行代码块。他会一直这样重复的操作下去,直到我们的while条件为false,也就是n的值小于或等于1。

而在递归函数中,函数每次调用的阶乘函数都会被添加到堆栈中,直到我们的if条件为false时,也就是n的值小于或等于1。这说明我们的阶乘函数将在执行之前被添加到堆栈中6次。这也就是为什么当我们尝试计算一个非常大的数(比如100,000)的阶乘时,会遇到"RangeError: Maximum call stack size exceeded"的错误的原因,因为调用栈中没有足够的空间来存储所有对阶乘函数的调用

但是如果使用尾调用优化就能解决上面的问题。

尾调用优化

每当一个函数的最后一件事是调用另一个函数时,那么这个最后一个函数不需要返回给它的调用者。因此,不需要在调用栈上存储任何信息,函数的调用更像是一个跳转。这种类型的调用被称为尾调用;不增加栈的操作被称为尾调用优化(TCO)。

那我们要怎么把它变成尾递归呢?

当然是借助另一个函数啦

// 计算正整数 n 的阶乘
const factorial = (n) => {
  // 使用 factorialHelper 函数来执行实际计算,初始累积值为 1
  return factorialHelper(n, 1);
}

// 辅助函数,递归计算阶乘
const factorialHelper = (x, accumulator) => {
  // 如果 x 小于或等于 1,返回累积值作为阶乘结果
  if (x <= 1) {
    return accumulator;
  }

  // 否则,递归调用自身,将 x 递减并累积的结果递归传递下去
  return factorialHelper(x - 1, x * accumulator);
}

上面,我们已经把函数修改成尾递归的形式了,它的最后一步是调用一个函数(而不是计算一个表达式,就像迭代一样)。接下来,让我们看看如何使用新的阶乘函数进行替代模型计算6的阶乘:

image.png

虽然我们现在的性能已经优于我们的递归函数,但是它还是会比迭代函数略微逊色。然而,如果我们计算较大的阶乘,依然会遇到"RangeError: Maximum call stack size exceeded"的错误。

但是为什么还会发生这种情况呢?

这是因为尽管我们的函数是尾递归的,但如果我们使用的是的Node.js和浏览器(除了Safari)来执行代码,他们都是不会进行尾调用优化

那么,我们应该要如何来解决这个问题呢?

答案是:可以借助另一个函数来实现!我们将依赖Trampoline(跳板)方法

Trampoline

// trampoline 函数用于实现尾递归优化
const trampoline = (fn) => {
  // 只要 fn 是函数,就不断执行它
  while (typeof fn === 'function') {
    fn = fn();
  }

  // 返回最终的非函数结果
  return fn;
}

我们的 Trampoline 函数由一个循环组成,该循环调用一个包装另一个函数的函数(我们称之为thunk),他会一直循环执行,直到符合条件才会退出。

// trampoline 函数用于实现尾递归,接受一个函数 fn 作为输入
const trampoline = (fn) => {
  // 只要 fn 是函数,就不断执行它,直到 fn 不再是函数为止
  while (typeof fn === 'function') {
    fn = fn();
  }

  // 返回最终的结果
  return fn;
}

// factorialHelper 函数用于递归计算阶乘,接受两个参数 x 和 accumulator
const factorialHelper = (x, accumulator) => {
  // 如果 x 小于等于 1,返回累积值(基本情况)
  if (x <= 1) {
    return accumulator;
  }

  // 返回一个函数,该函数会在下一次迭代中计算下一个递归步骤
  return () => factorialHelper(x - 1, x * accumulator);
}

// factorial 函数用于启动阶乘计算,接受一个参数 n
const factorial = (n) => {
  // 调用 trampoline 函数,将输入参数 n 和初始累积值 1 传递给 factorialHelper
  return trampoline(() => factorialHelper(n, 1));
}


现在,我们可以来计算一个比较大的阶乘了,并且再也不会担心出现RangeError: Maximum call stack size exceeded错误了。

但是如果我们要计算的阶乘是无穷大的,因为它是一个非常大的数字(大于 Number.MAX_SAFE_INTEGER: 253 - 1的数字)。在这种情况下,我们可以使用BigInt。

// trampoline 函数用于实现尾递归优化,接受一个函数 fn 作为输入
const trampoline = (fn) => {
  // 只要 fn 是函数,就不断执行它,直到 fn 不再是函数为止
  while (typeof fn === 'function') {
    fn = fn()
  }

  // 返回最终的结果
  return fn
}

// factorialHelper 函数用于递归计算阶乘,接受两个参数 x 和 accumulator
const factorialHelper = (x, accumulator) => {
  // 如果 x 小于等于 1,返回累积值(基本情况)
  if (x <= 1) {
    return accumulator
  }

  // 否则,返回一个函数,该函数将计算下一个递归步骤
  return () => factorialHelper(x - 1n, x * accumulator)
}

// factorial 函数用于启动阶乘计算,接受一个参数 n
const factorial = (n) => {
  // 将输入值 n 和初始累积值 1 转换为 BigInt 数据类型,然后调用 trampoline 函数进行尾递归计算
  return trampoline(factorialHelper(BigInt(n), 1n))
}

最后我们可以给函数添加类型定义,也就是ts的写法

// 定义 Thunk 类型,它可以是 bigint 或返回 Thunk 的函数
type Thunk = bigint | (() => Thunk)

// trampoline 函数用于实现尾递归,接受一个 Thunk 作为输入
const trampoline = (fn: Thunk) => {
  // 只要 fn 是函数,就循环执行它,直到 fn 不再是函数为止
  while (typeof fn === 'function') {
    fn = fn()
  }

  // 返回最终的结果,可能是 bigint
  return fn
}

// factorialHelper 函数用于递归计算阶乘,接受两个参数 x 和 accumulator,并返回一个 Thunk
const factorialHelper = (x: bigint, accumulator: bigint): Thunk => {
  // 如果 x 小于等于 1,返回累积值(base case)
  if (x <= 1n) {
    return accumulator
  }

  // 否则,返回一个函数,该函数将计算下一个递归步骤
  return () => factorialHelper(x - 1n, x * accumulator)
}

// factorial 函数用于启动阶乘计算,接受一个 number 类型的参数 n
const factorial = (n: number) => {
  // 使用 trampoline 函数调用 factorialHelper,传入 bigint 参数,初始累积值为 1n
  return trampoline(factorialHelper(BigInt(n), 1n))
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1991920.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

京东健康·全球医疗AI创新大赛初赛圆满结束!

近日&#xff0c;京东健康全球医疗AI创新大赛的初赛已顺利落下帷幕。本次大赛由京东健康发起&#xff0c;旨在探索医疗行业前沿技术与创新应用&#xff0c;携手产学研各界力量&#xff0c;推动医疗服务行业的高质量发展。 大赛聚焦“睡眠监测智能算法”与“医疗大模型创新应用”…

零基础入门转录组数据分析——机器学习算法之boruta(训练模型)

零基础入门转录组数据分析——机器学习算法之boruta&#xff08;训练模型&#xff09; 目录 零基础入门转录组数据分析——机器学习算法之boruta&#xff08;训练模型&#xff09;1. boruta基础知识2. boruta&#xff08;Rstudio&#xff09;——代码实操2. 1 数据处理2. 2 构建…

临床预测模型概述6-统计模型实操-单/多因素Cox回归

基础知识回顾&#xff1a; https://mp.weixin.qq.com/s/pXRZ1rYUr3lwH5OlDeB0_Q https://mp.weixin.qq.com/s/UVR6ZHCwhWqTfFBmPYPV9Q 接下来我们进行cox回归模型的实际操练。 简单回顾一下cox回归&#xff0c;在各种临床/基础数据分析中&#xff0c;经常需要分析各种影响/…

学校考场电子钟设置自动开关机,节能环保

在标准化考试中&#xff0c;准确的时间显示对于确保考试的公正性和秩序至关重要。然而&#xff0c;传统的电子钟系统往往存在一些问题&#xff0c;影响了考试管理的效率。 一、学校普通电子钟使用问题 二、学校考场电子钟优点 学校同步时钟系统通过自动同步网络或卫星时间的方式…

排序算法3:归并排序与计数排序

前言 Hello&#xff0c;小伙伴们&#xff0c;今天我们继续排序算法的学习&#xff0c;大家三连上车不迷路&#xff0c;我们现在开始今天的学习&#xff01;&#xff01;&#xff01; 1.归并排序 1.1归并排序的算法思想 归并排序&#xff08;MERGE_SORT&#xff09;是建立在归…

洗地机什么牌子最好?洗地机排行榜前十名大总结

在追寻现代家庭清洁新境界的旅程中&#xff0c;洗地机凭借其卓越的清洁效率成为了焦点。以下是我基于深入的市场调研、个人亲身体验以及广泛收集的用户反馈&#xff0c;精心整理出的一份洗地机品牌排行榜及特色亮点解析。请注意&#xff0c;以下排名并不代表绝对的优劣顺序&…

会展中心定位导航:一站式展会解决方案,招商管理系统与展位精准指引的数字化

在当今数字化浪潮下&#xff0c;会展中心作为连接全球商贸与文化的桥梁&#xff0c;其运营效率与参展体验成为衡量成功与否的关键指标。随着物联网、大数据及AI技术的飞速发展&#xff0c;智能定位导航系统应运而生&#xff0c;为会展中心带来了前所未有的变革。会展中心定位导…

线下支付场景之反扫支付

很多老板都在后台私信小编&#xff0c;你们牛卡派这些支付功能确实不错&#xff0c;但只局限于线上网站的场景&#xff0c;可我们没有技术&#xff0c;只是线下来收收款&#xff0c;有没有这方面的产品&#xff1f; 当然有&#xff01;&#xff01;我们牛卡派不仅专注于线上的支…

取消Edge浏览器自带的JSON格式化插件

取消Edge浏览器自带的JSON格式化插件 Edge 浏览器自带了 JSON 转换功能&#xff0c;即某个请求响应的是 JSON格式的数据的话 Edge浏览器则会完成自动转换。如图所示&#xff1a; 这样一来如果自己想要安装其他美观的 JSON 格式化工具则会发现不生效。如JSON Formatter 工具。 …

【YashanDB数据库】由于网络带宽不足导致的jdbc向yashandb插入数据慢

问题现象 某客户环境&#xff0c;客户的业务使用jdbc驱动向其他操作系统上的yashandb插入90万条数据&#xff0c;耗时大约30分钟。 问题的风险及影响 影响客户的业务处理效率 问题影响的版本 所有的yashandb版本 问题发生原因 jdbc执行batch insert时&#xff0c;是有绑…

基于Springboot + Vue的宿舍管理系统

前言 文末获取源码数据库 感兴趣的可以先收藏起来&#xff0c;需要学编程的可以给我留言咨询&#xff0c;希望帮助更多的人 精彩专栏推荐订阅 不然下次找不到哟 Java精品毕设原创实战项目 作者的B站地址&#xff1a;程序员云翼的个人空间-程序员云翼个人主页-哔哩哔哩视频 csd…

idm下载速度只有1m IDM下载速度不稳定什么原因 idm下载速度慢解决办法

IDM能够集成到大多数流行的浏览器中&#xff0c;并提供一键下载功能&#xff0c;方便用户快速下载网页中的文件。今天&#xff0c;我们学习IDM下载速度只有1M&#xff0c; IDM下载速度不稳定什么原因的内容。 一、 IDM下载速度只有1M的原因 IDM&#xff08;Internet Download…

红酒与书籍:品味文字与酒香的双重享受

在静谧的夜晚&#xff0c;当一杯洒派红酒&#xff08;Bold & Generous&#xff09;与一本好书相遇&#xff0c;便是一场品味文字与酒香的双重享受。红酒的醇厚与书籍的深邃相互交织&#xff0c;带来的是一场心灵的盛宴&#xff0c;让人沉醉其中&#xff0c;忘却尘世的喧嚣。…

87. UE5 RPG 实现技能面板技能的升级降级功能

在上一篇文章里&#xff0c;我们实现技能面板里的技能按钮配置数据&#xff0c;在角色对应的等级后&#xff0c;会解锁对应的技能&#xff0c;并实现了监听玩家角色所拥有的技能点数。 在这一篇里&#xff0c;我们将实现&#xff0c;通过玩家所拥有的技能点数&#xff0c;对技能…

AI入门指南(二):算法、训练、模型、大模型是什么?

文章目录 一、前言二、算法是什么&#xff1f;概念实际应用 三、训练是什么&#xff1f;概念实际应用 四、模型是什么&#xff1f;概念实际应用小结 五、大模型是什么&#xff1f;概念模型和小模型有什么区别&#xff1f;大模型分类实际应用 六、总结七、参考资料 一、前言 人…

宠物空气净化器对浮毛有效果吗?宠物空气净化器十大排名使用体验

宠物空气净化器对浮毛有效果吗&#xff1f;宠物空气净化器能够通过过滤和吸附作用&#xff0c;减少室内空气中的浮毛。不过&#xff0c;随着宠物空气净化器市场的火爆&#xff0c;许多商家企图从中分一杯羹&#xff0c;为了快速的进入市场缺少产品的研发&#xff0c;导致市面上…

海南云亿商务咨询有限公司怎么样正规吗?

在这个日新月异的数字时代&#xff0c;电商行业正以前所未有的速度蓬勃发展&#xff0c;而抖音作为短视频与社交电商的完美融合体&#xff0c;更是开辟了一片充满无限可能的电商新蓝海。在这片浪潮中&#xff0c;海南云亿商务咨询有限公司凭借敏锐的市场洞察力和专业的服务能力…

docker部署datahub

快速安装&#xff0c;有网环境用&#xff0c;但是我没有用 pip3 install datahub datahub docker quickstart一、安装并启动datahub #python安装客户端 pip3 install datahub #导入镜像 (base) [rootb28-16p4p170-lijia lijia]# ls *.tar cp-kafka.tar datahub-ac…

【ARM】ARM Cortex 处理器详细讲解

目录 ARM Cortex处理器详细讲解1. Cortex-A系列1.1 概述1.2 架构和版本**ARMv7-A****ARMv8-A****ARMv9-A** 1.3 关键特性1.4 应用场景 2. Cortex-M系列2.1 概述2.2 架构和版本**ARMv6-M****ARMv7-M****ARMv8-M** 2.3 关键特性2.4 应用场景 3. Cortex-R系列3.1 概述3.2 架构和版…

SVM支持向量机(Support Vectors Mechine)

SVM支持向量机&#xff08;Support Vector Mechine&#xff09; SVM是一种监督学习算法&#xff0c;常用于解决二分类问题&#xff08;也可以解决多分类问题&#xff09;&#xff0c;它可以处理线性可分的数据&#xff0c;同时也可以处理线性不可分的数据&#xff0c;广泛应用…