js 中的递归应用+异步递归

文章目录

    • 递归详解
    • 递归算法优化
    • 复杂应用中递归应用
    • 递归过程中应该注意的一些事
    • 异步递归及实例

递归详解

  1. 尾递归优化
    • 原理:尾递归是指在函数的最后一步调用自身。在这种情况下,编译器或解释器可以通过优化,将递归调用转换为循环,从而避免栈的不断增长。因为在尾递归中,当前函数的栈帧在递归调用返回后就不需要了,所以可以被复用。
    • 示例:以计算阶乘为例,普通递归函数如下:
    function factorial(n) {if (n === 0) {return 1;}return n * factorial(n - 1);
    }
    
    尾递归优化后的代码如下:
    function factorialTail(n, accumulator = 1) {if (n === 0) {return accumulator;}return factorialTail(n - 1, n * accumulator);
    }
    
    在尾递归版本中,accumulator参数用于累计计算结果。每次递归调用时,n的值减1,同时accumulator乘以当前的n值。当n为0时,直接返回accumulator的值,这样就避免了栈的过度增长。不过,需要注意的是,JavaScript引擎目前对尾递归优化的支持并不完善,在某些环境下可能仍然会出现栈溢出的情况。
  2. 循环替代递归
    • 原理:将递归算法转换为循环结构,这样就不会受到函数调用栈深度的限制。在很多情况下,递归算法可以通过使用栈(或队列)数据结构来模拟函数调用栈的行为,从而实现相同的功能。
    • 示例:还是以计算斐波那契数列为例,原始递归函数如下:
    function fibonacci(n) {if (n === 0 || n === 1) {return n;}return fibonacci(n - 1)+fibonacci(n - 2);
    }
    
    使用循环来优化后的代码如下:
    function fibonacciLoop(n) {if (n === 0 || n === 1) {return n;}let a = 0;let b = 1;let result;for (let i = 2; i <= n; i++) {result = a + b;a = b;b = result;}return result;
    }
    
    在循环版本中,通过使用ab两个变量来保存斐波那契数列的前两项的值,然后在循环中不断更新这两个变量,计算出第n项的值。这样就避免了递归调用带来的栈溢出问题。
  3. 记忆化(Memoization)
    • 原理:记忆化是一种优化技术,用于存储函数调用的结果,以便在后续相同的参数调用时直接返回存储的结果,而不是重新计算。对于递归函数,尤其是那些有重复计算的函数(如斐波那契数列函数),记忆化可以大大减少计算量,从而间接避免栈溢出。
    • 示例:以下是使用记忆化优化斐波那契数列函数的代码:
    const memo = {};
    function fibonacciMemo(n) {if (n === 0 || n === 1) {return n;}if (memo[n]) {return memo[n];}const result = fibonacciMemo(n - 1)+fibonacciMemo(n - 2);memo[n]=result;return result;
    }
    
    在这个函数中,使用一个memo对象来存储已经计算过的斐波那契数列的值。当调用fibonacciMemo函数时,首先检查memo对象中是否已经有了n对应的结果,如果有则直接返回;如果没有,则进行计算,并将结果存储到memo对象中,以便后续使用。这样可以避免大量重复计算,减少函数调用次数,从而降低栈溢出的风险。

递归算法优化

  1. 尾递归优化
    • 原理:尾递归是指在函数的最后一步调用自身。在这种情况下,编译器或解释器可以通过优化,将递归调用转换为循环,从而避免栈的不断增长。因为在尾递归中,当前函数的栈帧在递归调用返回后就不需要了,所以可以被复用。
    • 示例:以计算阶乘为例,普通递归函数如下:
    function factorial(n) {if (n === 0) {return 1;}return n * factorial(n - 1);
    }
    
    尾递归优化后的代码如下:
    function factorialTail(n, accumulator = 1) {if (n === 0) {return accumulator;}return factorialTail(n - 1, n * accumulator);
    }
    
    在尾递归版本中,accumulator参数用于累计计算结果。每次递归调用时,n的值减1,同时accumulator乘以当前的n值。当n为0时,直接返回accumulator的值,这样就避免了栈的过度增长。不过,需要注意的是,JavaScript引擎目前对尾递归优化的支持并不完善,在某些环境下可能仍然会出现栈溢出的情况。
  2. 循环替代递归
    • 原理:将递归算法转换为循环结构,这样就不会受到函数调用栈深度的限制。在很多情况下,递归算法可以通过使用栈(或队列)数据结构来模拟函数调用栈的行为,从而实现相同的功能。
    • 示例:还是以计算斐波那契数列为例,原始递归函数如下:
    function fibonacci(n) {if (n === 0 || n === 1) {return n;}return fibonacci(n - 1)+fibonacci(n - 2);
    }
    
    使用循环来优化后的代码如下:
    function fibonacciLoop(n) {if (n === 0 || n === 1) {return n;}let a = 0;let b = 1;let result;for (let i = 2; i <= n; i++) {result = a + b;a = b;b = result;}return result;
    }
    
    在循环版本中,通过使用ab两个变量来保存斐波那契数列的前两项的值,然后在循环中不断更新这两个变量,计算出第n项的值。这样就避免了递归调用带来的栈溢出问题。
  3. 记忆化(Memoization)
    • 原理:记忆化是一种优化技术,用于存储函数调用的结果,以便在后续相同的参数调用时直接返回存储的结果,而不是重新计算。对于递归函数,尤其是那些有重复计算的函数(如斐波那契数列函数),记忆化可以大大减少计算量,从而间接避免栈溢出。
    • 示例:以下是使用记忆化优化斐波那契数列函数的代码:
    const memo = {};
    function fibonacciMemo(n) {if (n === 0 || n === 1) {return n;}if (memo[n]) {return memo[n];}const result = fibonacciMemo(n - 1)+fibonacciMemo(n - 2);memo[n]=result;return result;
    }
    
    在这个函数中,使用一个memo对象来存储已经计算过的斐波那契数列的值。当调用fibonacciMemo函数时,首先检查memo对象中是否已经有了n对应的结果,如果有则直接返回;如果没有,则进行计算,并将结果存储到memo对象中,以便后续使用。这样可以避免大量重复计算,减少函数调用次数,从而降低栈溢出的风险。

复杂应用中递归应用

  1. 理解循环和递归的组合场景
    • 在复杂应用中,循环用于处理一组相关的任务或数据集合,而递归用于处理具有自相似结构的子任务。例如,在处理树形结构数据(如文件系统目录树、组织结构树等)时,外层循环可能用于遍历树的每一层,而递归函数用于深入处理每个节点的子树。
  2. 示例:遍历多层嵌套的JSON数据结构
    • 假设我们有一个多层嵌套的JSON数据结构,代表一个公司的部门组织结构。数据结构可能如下:
    {"name": "公司总部","departments": [{"name": "研发部","subdepartments": [{"name": "前端组","subdepartments": []},{"name": "后端组","subdepartments": []}]},{"name": "市场部","subdepartments": [{"name": "广告组","subdepartments": []},{"name": "销售组","subdepartments": []}]}]
    }
    
    • 我们可以使用循环和递归组合来遍历这个数据结构。外层循环可以用于遍历当前层的部门,而递归函数用于深入处理每个部门的子部门。
    • 以下是JavaScript代码实现:
    const companyStructure = {// 上面的JSON数据结构
    };
    function traverseDepartments(departments) {for (let i = 0; i < departments.length; i++) {const department = departments[i];console.log(department.name);if (department.subdepartments.length > 0) {traverseDepartments(department.subdepartments);}}
    }
    traverseDepartments(companyStructure.departments);
    
    • 在这个代码中,traverseDepartments函数接受一个部门数组作为参数。外层循环遍历数组中的每个部门,首先打印部门名称。然后检查部门是否有子部门(subdepartments.length > 0),如果有,则递归调用traverseDepartments函数来深入处理子部门。这样就可以遍历整个多层嵌套的组织结构。
  3. 注意事项
    • 控制递归深度:在复杂应用中,由于数据结构的复杂性,很容易导致递归深度过深。为了避免栈溢出,可以采用前面提到的优化递归算法的方法,如尾递归优化(如果语言或环境支持)、记忆化或者限制递归深度。
    • 数据一致性和状态管理:在循环中使用递归时,要注意数据的一致性。例如,在处理树形结构时,可能需要维护一些关于当前节点状态的信息,如当前路径、节点层次等。这些信息在递归调用过程中需要正确地传递和更新,以确保程序的正确运行。
    • 错误处理和边界条件:要仔细考虑边界条件和可能出现的错误情况。例如,在上述组织结构遍历的例子中,需要考虑部门没有子部门(subdepartments.length === 0)的情况,以及数据结构可能不符合预期的情况(如缺少关键属性)。在递归函数和循环中都需要有适当的错误处理机制,以增强程序的健壮性。

递归过程中应该注意的一些事

  1. 栈溢出风险
    • 易错点
      • 没有正确设置递归的终止条件(基线条件)很容易导致无限递归,从而引发栈溢出。例如,在计算阶乘的递归函数中,如果忘记了n === 0n === 1时返回1这个终止条件,函数会一直调用自身,快速耗尽栈空间。
    • 注意事项
      • 务必确保递归函数有明确的基线条件,并且在合适的情况下能够终止递归。在编写递归函数时,要仔细思考问题的边界情况,比如在处理数列计算时,要考虑起始值(如斐波那契数列中n = 0n = 1的情况)。同时,可以采用一些策略来优化以避免栈溢出,如尾递归优化(如果语言或环境支持)、用循环替代递归或者进行记忆化处理。
  2. 性能问题
    • 易错点
      • 一些递归算法可能会因为重复计算而导致性能低下。以斐波那契数列的简单递归实现为例,在计算fibonacci(n)时,fibonacci(n - 1)fibonacci(n - 2)的计算过程中有很多重复的子计算。例如,计算fibonacci(5)时,fibonacci(3)会被多次计算。
    • 注意事项
      • 考虑使用记忆化来避免重复计算。记忆化可以通过一个数据结构(如对象或数组)来存储已经计算过的结果,在后续调用中直接使用存储的结果,而不是重新计算。这在处理具有重叠子问题的递归算法时(如动态规划问题)非常有效,可以显著提高性能。
  3. 参数传递和状态管理
    • 易错点
      • 在递归过程中,参数传递错误或者没有正确管理状态会导致结果错误。例如,在一个递归函数用于遍历树结构时,如果没有正确传递当前节点的子节点列表或者没有正确更新当前节点的索引等相关参数,可能会导致遍历不完整或者错误。
    • 注意事项
      • 仔细考虑每次递归调用时需要传递哪些参数,确保参数能够准确地反映当前的问题状态。对于复杂的状态,可以使用额外的数据结构来辅助管理,如在遍历树结构时,可以使用一个栈来记录节点的路径或者使用一个对象来记录节点的访问状态。同时,在每次递归调用后,要确保参数的更新符合问题的逻辑,比如在处理链表的递归操作时,要正确更新指针。
  4. 理解递归调用的顺序和逻辑
    • 易错点
      • 新手可能会错误地理解递归调用的顺序,导致对函数执行流程的误解。例如,在一个多层递归函数中,可能会错误地认为内层递归调用结束后,外层函数的状态会自动恢复到调用内层递归之前的状态,而忽略了需要正确地处理返回值和状态更新。
    • 注意事项
      • 仔细研究递归函数的调用顺序和返回值处理。可以通过添加调试语句(如console.log)来跟踪递归函数的执行过程,清晰地了解每次调用的参数、返回值以及对状态的影响。同时,对于复杂的递归逻辑,可以画一个简单的流程图或者使用示例数据手动模拟函数的执行过程,帮助理解递归的顺序和逻辑。

异步递归及实例

  1. 异步递归的应用场景

    • 网络爬虫

      • 场景描述:在爬取多层嵌套的网页结构时很有用。例如,一个网页可能包含多个链接,每个链接指向的网页又可能包含更多链接。为了爬取整个网站的内容,需要递归地访问这些链接。由于网络请求是异步的,需要使用异步递归。
      • 示例:假设要爬取一个博客网站,首页有多个文章链接,每个文章页可能又有相关文章链接。可以使用异步函数来发送HTTP请求获取网页内容,然后在解析内容找到新链接后,异步递归地发送请求获取新链接指向的网页内容。
    • 文件系统操作

      • 场景描述:在处理具有多层目录结构的文件系统时,异步递归非常适用。例如,要遍历一个目录及其子目录下的所有文件,对每个文件进行某种异步操作(如读取文件内容并进行加密处理)。
      • 示例:在Node.js中,使用fs.readdir函数读取目录内容,对于每个子目录,异步递归地调用相同的遍历函数;对于每个文件,使用fs.readFile函数异步读取文件内容。
    • 数据库操作

      • 场景描述:当数据库中的数据具有层次结构,且需要异步地处理这些数据时会用到异步递归。例如,在一个包含分类和子分类的商品数据库中,可能需要从根分类开始,递归地查询每个分类及其子分类下的商品信息,并且数据库查询操作通常是异步的。
      • 示例:假设使用Node.js和一个支持异步操作的数据库驱动(如mongoose用于MongoDB),从一个顶层分类开始,异步查询其下的子分类,对于每个子分类,又异步递归地查询更下一层的子分类和对应的商品信息。
  2. 异步递归的解决方案

    • 使用Promise和async/await

      • 原理:在JavaScript中,async/await语法糖是基于Promise实现的,它使得异步代码看起来更像同步代码,便于理解和维护。对于异步递归,可以将异步操作封装在async函数中,然后在函数内部使用await来暂停执行,直到异步操作完成。
      • 示例:异步读取多层目录下的文件内容
      const fs = require('fs').promises;
      async function readFilesRecursive(directory) {const files = await fs.readdir(directory);for (const file of files) {const filePath = `${directory}/${file}`;const stats = await fs.stat(filePath);if (stats.isDirectory()) {await readFilesRecursive(filePath);} else {const content = await fs.readFile(filePath, 'utf8');console.log(content);}}
      }
      readFilesRecursive('.');
      

      在这个示例中,readFilesRecursive是一个async函数,它首先使用await fs.readdir获取目录下的文件列表。然后对于每个文件或子目录,通过await fs.stat判断类型。如果是子目录,就异步递归地调用readFilesRecursive;如果是文件,就使用await fs.readFile读取文件内容并打印。

    • 使用回调函数(Callback)

      • 原理:回调函数是一种传统的异步编程方式。在递归函数中,将下一层递归调用或者后续操作封装在回调函数中,当异步操作完成后,调用这个回调函数来继续执行后续步骤。
      • 示例:简单的异步回调递归示例(模拟异步操作)
      function asyncRecursive(i, callback) {setTimeout(() => {console.log(i);if (i > 0) {asyncRecursive(i - 1, callback);} else {callback();}}, 1000);
      }
      asyncRecursive(3, () => {console.log('递归完成');
      });
      

      在这个示例中,asyncRecursive函数模拟了一个异步操作(使用setTimeout)。每次异步操作完成后(经过1秒),打印当前的i值,然后如果i大于0,就继续递归调用asyncRecursive。当i为0时,调用传入的callback函数,表示递归完成。不过,这种方式可能会导致回调地狱(Callback Hell)问题,代码的可读性和可维护性相对较差,尤其是在复杂的异步递归场景中。

    • 使用事件发射器(Event Emitter)

      • 原理:事件发射器可以用来处理异步事件的触发和监听。在异步递归场景中,可以在每次异步操作完成后触发一个事件,而递归函数可以监听这个事件来决定是否继续下一层递归或者进行其他操作。
      • 示例:简单的事件发射器异步递归示例(使用Node.js的events模块)
      const EventEmitter = require('events');
      const emitter = new EventEmitter();
      let count = 3;
      function asyncRecursiveWithEmitter() {setTimeout(() => {console.log(count);count--;if (count >= 0) {emitter.emit('next');}}, 1000);
      }
      emitter.on('next', asyncRecursiveWithEmitter);
      asyncRecursiveWithEmitter();
      

      在这个示例中,asyncRecursiveWithEmitter函数模拟了一个异步操作(使用setTimeout)。每次异步操作完成后(经过1秒),打印当前的count值,然后将count减1。如果count大于等于0,就通过事件发射器emitter触发一个next事件。同时,递归函数通过监听next事件来决定是否继续下一层递归。这种方式可以将异步操作和递归逻辑分离,一定程度上提高代码的可维护性。

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

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

相关文章

手机租赁平台开发全攻略打造高效便捷的租赁服务系统

内容概要 手机租赁平台开发&#xff0c;简单说就是让用户能轻松租赁各类手机的高效系统。这一平台不仅帮助那些想要临时使用高端手机的人们节省了不少资金&#xff0c;还为商家开辟了新的收入渠道。随着智能手机的普及&#xff0c;很多人并不需要长期拥有一部手机&#xff0c;…

[最佳方法] 如何将视频从 Android 发送到 iPhone

概括 将大视频从 Android 发送到 iPhone 或将批量视频从 iPhone 传输到 Android 并不是一件容易的事情。也许您已经尝试了很多关于如何将视频从 Android 发送到 iPhone 15/14 的方法&#xff0c;但都没有效果。但现在&#xff0c;通过本文中的这 6 种强大方法&#xff0c;您可…

记录一下图像处理的基础知识

记录一下自己学习的图像处理的基础知识。 一、图像的文件格式以及常用的图像空间 1、文件格式 常见的图像文件格式有 jpg, png, bmp, gif &#xff08;1&#xff09;jpg&#xff1a;有损压缩算法&#xff0c;大幅减小文件大小&#xff0c;便于存储和传输&#xff0c;兼容性…

算法-各位数相加,直至和为个位数

给定一个非负整数 num&#xff0c;反复将各个位上的数字相加&#xff0c;直到结果为一位数。返回这个结果。 示例 1: 输入: num 38 输出: 2 解释: 各位相加的过程为&#xff1a; 38 --> 3 8 --> 11 11 --> 1 1 --> 2 由于 2 是一位数&#xff0c;所以返回 2。…

Openwrt 下移植 源码安装Cmake

Openwrt 下源码编译安装Cmake cmake介绍源码下载安装configure问题/usr/bin/ld: cannot find -ldlCould NOT find OpenSSL运行CMake Error: Could not find CMAKE_ROOT !!!Openwrt opkg不支持cmake安装,本文尝试在目标板上基于cmake源码编译安装cmake, 并将遇到的问题和解决方…

使用Python,networkx构造有向图及无向图以及图合并等api

使用Python&#xff0c;networkx构造有向图及无向图以及图合并等api 源码图的构造、节点及边的添加等有向图及无向图及多重图 参考 方法名方法作用subgraph(G, nbunch)返回包含nbunch节点的子图union(G, H[, rename])合并G和H图disjoint_union(G, H)合并G和H图cartesian_produc…

【Java回顾】Day3 继承|Override/Ovverload|多态|抽象类|封装|接口|枚举

学习资料 菜鸟教程 https://www.runoob.com/java/java-interfaces.html 继承|Override/Ovverload|多态|抽象类|封装|接口|枚举 继承 创建分等级层次的类&#xff0c;子类继承父类的特征、行为、方法 class 父类{ } class 子类 extends 父类{ super(); }一些性质 Java 不支持…

2025年AI和AR谁才是智能眼镜的未来

在2025年&#xff0c;智能眼镜市场正迎来一场技术革新的浪潮&#xff0c;其中AI和AR技术的竞争尤为激烈。那么&#xff0c;究竟谁才是智能眼镜的未来呢&#xff1f;让我们来一探究竟。 AI眼镜的崛起 AI眼镜通过集成人工智能技术&#xff0c;提供了语音识别、环境感知和个性化服…

java实现预览服务器文件,不进行下载,并增加水印效果

通过文件路径获取文件&#xff0c;对不同类型的文件进行不同处理&#xff0c;将Word文件转成pdf文件预览&#xff0c;并早呢更加水印&#xff0c;暂不支持Excel文件&#xff0c;如果浏览器不支持PDF文件预览需要下载插件。文中currentUser.getUserid()&#xff0c;即为增加的水…

快速上手大模型的对话生成

本项目使用0.5B小模型&#xff0c;结构和大模型别无二致&#xff0c;以方便在如CPU设备上快速学习和上手大模型的对话上传 #mermaid-svg-Z86hUiQZ0hg9BVji {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-Z86hUiQZ0h…

Unreal虚幻引擎使用遇到的问题记录

文章目录 The game module ‘MyGame’ could not be loaded. There may be an operating system error or the module may not be properly set up The game module ‘MyGame’ could not be loaded. There may be an operating system error or the module may not be properl…

在Unity中用Ab包加载资源(简单好抄)

第一步创建一个Editor文件夹 第二步编写BuildAb&#xff08;这个脚本一点要放在Editor中因为这是一个编辑器脚本&#xff0c;放在其他地方可能会报错&#xff09; using System.IO; using UnityEditor; using UnityEngine;public class BuildAb : MonoBehaviour {// 在Unity编…

丢弃法hhhh

一个好的模型需要对输入数据的扰动鲁棒 丢弃法&#xff1a;在层之间加入噪音&#xff0c;等同于加入正则 h2和h5变成0了 dropout一般作用在全连接隐藏层的输出上 Q&A dropout随机置零对求梯度和求反向传播的影响是什么&#xff1f;为0 dropout属于超参数 dropout固定随…

mysql 报错 ERROR 1396 (HY000) Operation ALTER USER failed for root@localhost 解决方案

参考:https://blog.csdn.net/m0_74824534/article/details/144177078 mysql 修改密码 ALTER USER ‘root’‘localhost’ IDENTIFIED BY ‘123’; 时&#xff0c;报错 ERROR 1396 (HY000): Operation ALTER USER failed for rootlocalhost 解决方案&#xff1a; 2024-4-3 段子…

Three.js Journey (notes2)

ref Three.js中文网 Three.js Journey — Learn WebGL with Three.js Part 1 Fullscreen and resizing When the double click happens, we will toggle the fullscreen —meaning that if the window is not in fullscreen, a double-click will enable fullscreen mode, …

C# 中 `new` 关键字的用法

在 C# 中&#xff0c;new 关键字用于修饰方法、属性、索引器或事件声明时&#xff0c;表示当前成员隐藏基类中同名的成员。它们之间的具体区别如下&#xff1a; 不加 new&#xff1a; 如果子类定义了一个与父类同名的方法&#xff0c;但没有使用 new 关键字&#xff0c;编译器会…

深入理解Python中的常用数据格式(如csv、json、pickle、npz、h5等):存储机制与性能解析

在数据科学与工程领域&#xff0c;数据的存储与读取是日常工作中不可或缺的一部分。选择合适的数据格式不仅影响数据处理的效率&#xff0c;还关系到存储空间的利用与后续分析的便捷性。本文将以通俗易懂的方式&#xff0c;深入探讨Python中几种常用的数据读写格式&#xff08;…

Ubuntu开机The root filesystem on /dev/sdbx requires a manual fsck 问题

出现“Manual fsck”错误可能由以下几种原因引起&#xff1a; 不正常关机&#xff1a;如果系统意外断电或被强制重启&#xff0c;文件系统可能未能正确卸载&#xff0c;导致文件系统损坏。磁盘故障&#xff1a;硬盘的物理损坏可能会引发文件系统错误。文件系统配置问题&#x…

Django Admin 以管理 AWS Lambda 函数

在现代云计算环境中,AWS Lambda 函数已成为构建无服务器应用程序的重要组成部分。作为开发者或运维工程师,有效管理这些 Lambda 函数是一项关键任务。今天,我们将探讨如何利用 Django Admin 创建一个强大而直观的界面来管理 AWS Lambda 函数。 背景 假设我们已经创建了一个…

黑马Java面试教程_P10_设计模式

系列博客目录 文章目录 系列博客目录前言1. 工厂方法模式1.1 概述1.2 简单工厂模式1.2.1 结构1.2.2 实现1.2.3 优缺点 1.3 工厂方法模式1.3.1 概念1.3.2 结构1.3.3 实现1.3.4 优缺点 1.4 抽象工厂模式1.4.1 概念1.4.2 结构1.4.3 实现1.4.4 优缺点1.4.5 使用场景 总结&#xff0…