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

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

// 阶乘函数,计算给定正整数 n 的阶乘
const factorial = (n) => {// 初始化结果为 1let result = 1;// 使用循环逐步计算阶乘while (n > 1) {// 乘以当前的 n 值result *= n;// 减小 n,准备下一次迭代n--;}// 返回最终的阶乘结果return result;
}

jcode

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

// 阶乘函数,递归方式计算给定正整数 n 的阶乘
const factorial = (n) => {// 当 n 为 0 时,阶乘为 1if (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 函数来执行实际计算,初始累积值为 1return 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 传递给 factorialHelperreturn 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()}// 返回最终的结果,可能是 bigintreturn 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 参数,初始累积值为 1nreturn trampoline(factorialHelper(BigInt(n), 1n))
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java之多线程篇

一、基本概念 1.什么是线程&#xff1f; 线程就是&#xff0c;操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。简单理解就是&#xff1a;应用软件中互相独立&#xff0c;可以同时运行的功能 2.什么是多线程&#xff1f; 有了多线…

无人机之飞行控制系统篇

一、飞行控制系统组成 包括惯性测量单位、GPS接收机、气压高度计、空速计等传感器&#xff0c;以及飞控计算机、伺服作动器等设备。 二、飞行控制原理 通过传感器实时感知无人机的飞行状态&#xff0c;将数据传输给飞控计算机进行处理&#xff0c;计算机再根据预设的飞行计划和…

13-按键的元件模型创建

1.画线的时候&#xff0c;栅格切为10mil 2.放置管脚的时候&#xff0c;栅格切为100mil

开发框架DevExpress XAF v24.2产品路线图预览——增强跨平台性

DevExpress XAF是一款强大的现代应用程序框架&#xff0c;允许同时开发ASP.NET和WinForms。XAF采用模块化设计&#xff0c;开发人员可以选择内建模块&#xff0c;也可以自行创建&#xff0c;从而以更快的速度和比开发人员当前更强有力的方式创建应用程序。 DevExpress XAF是一…

LLaMA- Adapter V2: Parameter-Efficient Visual Instruction Model

发表时间&#xff1a;28 Apr 2023 论文链接&#xff1a;https://arxiv.org/pdf/2304.15010 作者单位&#xff1a; Shanghai Artificial Intelligence Laboratory Motivation&#xff1a;如何有效地将大型语言模型 (LLM) 转换为指令追随者最近是一个流行的研究方向&#xff0…

Linux基于centOS7【内存与OS的随谈】,进程初学【PCB】【fork】【进程排队】

冯诺依曼体系结构——存储器 存储器主要指的是内存&#xff0c;它有个特点就是掉电易失 磁盘等其它输入和输出设备 为什么要在计算机体系结构中要存在内存 我们知道&#xff0c;CPU的处理速度很快很快&#xff0c;但输入设备&#xff0c;以及输出设备&#xff0c;是相对很慢的…

sql注入靶场搭建

1.安装小皮面板&#xff08;PhpStudy&#xff09; 1.从官网下载&#xff1a;http://www.xp.cn 2、Sqli-labs环境安装 准备好sqli-labs-php7-master文件 3.安装之前确保本地没有下载mysql服务器 如果电脑下载了MySQL可以把MySQL的服务停掉 此电脑>右键>管理>服务…

QModbus例程分析

由于有一个Modebus上位机的需要&#xff0c;分析一下QModbus Slave的源代码&#xff0c;方便后面的开发。 什么是Modbus Modbus是一种常用的串行通信协议&#xff0c;被广泛应用于工业自动化领域。它最初由Modicon&#xff08;目前属于施耐德电气公司&#xff09;于1979年开发…

C++:vector容器

概览 std::vector是C标准模板库(STL)中的一种动态数组容器。它提供了一种类似于数组的数据结构&#xff0c;但是具有动态大小和更安全的内存管理。 定义和基本特性 std::vector是C标准库中的一 个序列容器&#xff0c;它代表了能够动态改变大小的数组。与普通数组一样&#x…

模拟面试题1

目录 一、JVM的内存结构&#xff1f; 二、类加载器分为哪几类&#xff1f; 三、讲一下双亲委派机制 为什么要有双亲委派机制&#xff1f; 那你知道有违反双亲委派的例子吗&#xff1f; 四、IO 有哪些类型&#xff1f; 五、Spring Boot启动机制 六、Spring Boot的可执行…

关于儿童编程语言

青少年通常会通过Scratch或Python开始学习编程。在这两种语言中&#xff0c;代码的编写&#xff08;或者在Scratch中是构建&#xff09;方式类似于英语&#xff0c;这使得初学者更容易学习。Scratch的一个重要卖点是对视觉和运动感知学习者非常友好。这些代码块按颜色编码&…

最短路问题中的bellman-ford算法

最短路问题中的bellman-ford算法 题目 如果要处理单源最短路问题当中存在负权边的&#xff0c;那么就需要用到 bellman-ford算法和SPFA算法&#xff0c;一般情况下都是用 SPFA算法&#xff0c;除了有边数限制的情况只能用bellman-ford算法&#xff0c;比如下面这种 题目 给定…

混合密度网络Mixture Density Networks(MDN)

目录 简介1 介绍2 实现3 几个MDN的应用&#xff1a;参考 简介 平方和或交叉熵误差函数的最小化导致网络输出近似目标数据的条件平均值&#xff0c;以输入向量为条件。对于分类问题&#xff0c;只要选择合适的目标编码方案&#xff0c;这些平均值表示类隶属度的后验概率&#x…

《程序猿入职必会(10) · SpringBoot3 整合 MyBatis-Plus》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

Linux中DHCP服务器配置和管理

文章目录 一、DHCP服务1.1、DHCP的工作流程1.2、DHCP的工作模式1.3、dhcp的主要配置文件 二、安装DHCP服务2.1、更新yum源2.2、安装DHCP服务软件包2.3、配置DHCP服务2.4、启用DHCP服务&#xff08;解决报错&#xff09;2.4.1、查看dhcpd服务的状态和最近的日志条目2.4.2、查看与…

【网络安全】探索AI 聊天机器人工作流程实现RCE

未经许可,不得转载。 文章目录 前言正文前言 我发现了一个广泛使用的AI聊天机器人平台中的远程代码执行漏洞。该漏洞存在于聊天机器人的自定义工作流响应代码中,这些工作流允许开发人员通过创建定制的流程来扩展机器人的功能。 正文 在浏览自动化聊天机器人的多个特定功能…

AI界的“小钢炮“:MiniCPM-V 2.6 版本震撼发布!

MiniCPM-V 2.6 面壁智能推出了一款颠覆性的端侧AI多模态模型——MiniCPM-V 2.6。这个被亲切地称为"小钢炮"的模型&#xff0c;以其惊人的性能和极致的效率&#xff0c;向业界巨头发起了挑战。 MiniCPM-V 2.6 MiniCPM-V 2.6 是 MiniCPM-V 系列中最新、性能最佳的模型。…

算法板子:匈牙利算法——二分图的最大匹配

目录 1. 基础概念 &#xff08;1&#xff09;二分图的概念 &#xff08;2&#xff09; 匈牙利算法的作用 2. 代码 1. 基础概念 &#xff08;1&#xff09;二分图的概念 顶点集 V 分为两个集合&#xff0c;且图中每条边依附的两个顶点都分属于这两个子集&#xff0c;也就是第…

《UniverSeg: Universal Medical Image Segmentation》ICCV2023

摘要 这篇论文提出了一种名为 UniverSeg 的方法&#xff0c;它能够解决未见过的医学图像分割任务&#xff0c;而无需额外的训练。现有的深度学习模型通常无法泛化到新的解剖结构、图像模式或标签上。UniverSeg 利用一种新的 CrossBlock 机制&#xff0c;通过查询图像和定义新分…

倍福PLC数据 转 CCLink IE Field Basic项目案例

目录 1 案例说明 1 2 VFBOX网关工作原理 1 3 准备工作 2 4 设置倍福PLC 2 5 配置网关参数采集倍福PLC数据 4 6 使用CCLINK协议转发数据 7 7 三菱PLC连接网关的CCLINK的设置 8 8 案例总结 12 1 案例说明 设置倍福PLC&#xff0c;开通ADS通信设置网关采集倍福PLC数据把采集的数…