从JS角度直观理解递归的本质

让我们写一个函数 pow(x, n),它可以计算 xn 次方。换句话说就是,x 乘以自身 n 次。

有两种实现方式。

  1. 迭代思路:使用 for 循环:

    function pow(x, n) {let result = 1;// 在循环中,用 x 乘以 result n 次for (let i = 0; i < n; i++) {result *= x;}return result;
    }alert( pow(2, 3) ); // 8
    
  2. 递归思路:简化任务,调用自身:

    function pow(x, n) {if (n == 1) {return x;} else {return x * pow(x, n - 1);}
    }alert( pow(2, 3) ); // 8
    

pow(x, n) 被调用时,执行分为两个分支:

              if n==1  = x/
pow(x, n) =\else     = x * pow(x, n - 1)
  1. 如果 n == 1,所有事情都会很简单,这叫做 基础 的递归,因为它会立即产生明显的结果:pow(x, 1) 等于 x
  2. 否则,我们可以用 x * pow(x, n - 1) 表示 pow(x, n)。在数学里,可能会写为 x<sup>n</sup><span> </span>= x * x<sup>n-1</sup>。这叫做 一个递归步骤:我们将任务转化为更简单的行为(x 的乘法)和更简单的同类任务的调用(带有更小的 npow 运算)。接下来的步骤将其进一步简化,直到 n 达到 1

这是一个很简单的例子,我们从直觉上很容易理解递归,但是有很多人会有一种不可名状的困惑,这个问题可以总结为“为什么会这样?”

现在我们来研究一下递归调用是如何工作的。为此,我们会先看看函数底层的工作原理。

有关正在运行的函数的执行过程的相关信息被存储在其 执行上下文 中。

执行上下文 是一个内部数据结构,它包含有关函数执行时的详细细节:当前控制流所在的位置,当前的变量,this的值(此处我们不使用它),以及其它的一些内部细节。

一个函数调用仅具有一个与其相关联的执行上下文。

当一个函数进行嵌套调用时,将发生以下的事儿:

  • 当前函数被暂停;
  • 与它关联的执行上下文被一个叫做 执行上下文堆栈 的特殊数据结构保存;
  • 执行嵌套调用;
  • 嵌套调用结束后,从堆栈中恢复之前的执行上下文,并从停止的位置恢复外部函数。

让我们看看 pow(2, 3) 调用期间都发生了什么。

pow(2, 3)

在调用 pow(2, 3) 的开始,执行上下文(context)会存储变量:x = 2, n = 3,执行流程在函数的第 1 行。

我们将其描绘如下:

  • Context: { x: 2, n: 3, at line 1 } pow(2, 3)

这是函数开始执行的时候。条件 n == 1 结果为假,所以执行流程进入 if 的第二分支。

function pow(x, n) {if (n == 1) {return x;} else {return x * pow(x, n - 1);}
}alert( pow(2, 3) );

变量相同,但是行改变了,因此现在的上下文是:

  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

为了计算 x * pow(x, n - 1),我们需要使用带有新参数的新的 pow 子调用 pow(2, 2)

pow(2, 2)

为了执行嵌套调用,JavaScript 会在 执行上下文堆栈 中记住当前的执行上下文。

这里我们调用相同的函数 pow,但这绝对没问题。所有函数的处理都是一样的:

  1. 当前上下文被“记录”在堆栈的顶部。
  2. 为子调用创建新的上下文。
  3. 当子调用结束后 —— 前一个上下文被从堆栈中弹出,并继续执行。

下面是进入子调用 pow(2, 2) 时的上下文堆栈:

  • Context: { x: 2, n: 2, at line 1 } pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

新的当前执行上下文位于顶部(粗体显示),之前记住的上下文位于下方。

当我们完成子调用后 —— 很容易恢复上一个上下文,因为它既保留了变量,也保留了当时所在代码的确切位置。

请注意:

在上面的图中,我们使用“行(line)”一词,因为在我们的示例中,每一行只有一个子调用,但通常一行代码可能会包含多个子调用,例如 pow(…) + pow(…) + somethingElse(…)

因此,更准确地说,执行是“在子调用之后立即恢复”的。

pow(2, 1)

重复该过程:在第 5 行生成新的子调用,现在的参数是 x=2, n=1

新的执行上下文被创建,前一个被压入堆栈顶部:

  • Context: { x: 2, n: 1, at line 1 } pow(2, 1)
  • Context: { x: 2, n: 2, at line 5 } pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

此时,有 2 个旧的上下文和 1 个当前正在运行的 pow(2, 1) 的上下文。

出口

在执行 pow(2, 1) 时,与之前的不同,条件 n == 1 为真,因此 if 的第一个分支生效:

function pow(x, n) {if (n == 1) {return x;} else {return x * pow(x, n - 1);}
}

此时不再有更多的嵌套调用,所以函数结束,返回 2

函数完成后,就不再需要其执行上下文了,因此它被从内存中移除。前一个上下文恢复到堆栈的顶部:

  • Context: { x: 2, n: 2, at line 5 } pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

恢复执行 pow(2, 2)。它拥有子调用 pow(2, 1) 的结果,因此也可以完成 x * pow(x, n - 1) 的执行,并返回 4

然后,前一个上下文被恢复:

  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

当它结束后,我们得到了结果 pow(2, 3) = 8

本示例中的递归深度为:3

从上面的解释我们可以看出,递归深度等于堆栈中上下文的最大数量。

请注意内存要求。上下文占用内存,在我们的示例中,求 n 次方需要存储 n 个上下文,以供更小的 n 值进行计算使用。

而循环算法更节省内存:

function pow(x, n) {let result = 1;for (let i = 0; i < n; i++) {result *= x;}return result;
}

迭代 pow 的过程中仅使用了一个上下文用于修改 iresult。它的内存要求小,并且是固定了,不依赖于 n

任何递归都可以用循环来重写。通常循环变体更有效。

但有时重写很难,尤其是函数根据条件使用不同的子调用,然后合并它们的结果,或者分支比较复杂时。而且有些优化可能没有必要,完全不值得。

递归可以使代码更短,更易于理解和维护。并不是每个地方都需要优化,大多数时候我们需要一个好代码,这就是为什么要使用它。

既然循环更好,为什么要使用递归,接下来举个例子。

递归的另一个重要应用就是递归遍历。

假设我们有一家公司。人员结构可以表示为一个对象:

let company = {sales: [{name: 'John',salary: 1000}, {name: 'Alice',salary: 1600}],development: {sites: [{name: 'Peter',salary: 2000}, {name: 'Alex',salary: 1800}],internals: [{name: 'Jack',salary: 1300}]}
};

换句话说,一家公司有很多部门。

  • 一个部门可能有一 数组 的员工,比如,sales 部门有 2 名员工:John 和 Alice。
  • 或者,一个部门可能会划分为几个子部门,比如 development 有两个分支:sitesinternals,它们都有自己的员工。
  • 当一个子部门增长时,它也有可能被拆分成几个子部门(或团队)。
    例如,sites 部门在未来可能会分为 siteAsiteB。并且,它们可能会被再继续拆分。没有图示,脑补一下吧。

现在,如果我们需要一个函数来获取所有薪资的总数。我们该怎么做?

迭代方式并不容易,因为结构比较复杂。首先想到的可能是在 company 上使用 for 循环,并在第一层部分上嵌套子循环。但是,之后我们需要更多的子循环来遍历像 sites 这样的二级部门的员工…… 然后,将来可能会出现在三级部门上的另一个子循环?如果我们在代码中写 3-4 级嵌套的子循环来遍历单个对象, 那代码得多丑啊。

我们试试递归吧。

我们可以看到,当我们的函数对一个部门求和时,有两种可能的情况:

  1. 要么是由一个人员 数组 构成的“简单”的部门 —— 这样我们就可以通过一个简单的循环来计算薪资的总和。
  2. 或者它是一个有 N 个子部门的 对象 —— 那么我们可以通过 N 层递归调用来求每一个子部门的薪资,然后将它们合并起来。

第一种情况是由人员数组构成的部门,这种情况很简单,是最基础的递归。

第二种情况是我们得到的是对象。那么可将这个复杂的任务拆分成适用于更小部门的子任务。它们可能会被继续拆分,但很快或者不久就会拆分到第一种情况那样。

这个算法从代码来看可能会更简单:

let company = { // 是同一个对象,简洁起见被压缩了sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600 }],development: {sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }],internals: [{name: 'Jack', salary: 1300}]}
};// 用来完成任务的函数
function sumSalaries(department) {if (Array.isArray(department)) { // 情况(1)return department.reduce((prev, current) => prev + current.salary, 0); // 求数组的和} else { // 情况(2)let sum = 0;for (let subdep of Object.values(department)) {sum += sumSalaries(subdep); // 递归调用所有子部门,对结果求和}return sum;}
}alert(sumSalaries(company)); // 7700

代码很短也容易理解(希望是这样?)。这就是递归的能力。它适用于任何层次的子部门嵌套。

下面是调用图:

请添加图片描述

我们可以很容易地看到其原理:对于对象 {...} 会生成子调用,而数组 [...] 是递归树的“叶子”,它们会立即给出结果。

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

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

相关文章

opencv进阶 ——(九)图像处理之人脸修复祛马赛克算法CodeFormer

算法简介 CodeFormer是一种基于AI技术深度学习的人脸复原模型&#xff0c;由南洋理工大学和商汤科技联合研究中心联合开发&#xff0c;它能够接收模糊或马赛克图像作为输入&#xff0c;并生成更清晰的原始图像。算法源码地址&#xff1a;https://github.com/sczhou/CodeFormer…

如何快速找到 RCE

背景介绍 本文将分享国外白帽子在‘侦察’阶段如何快速发现 RCE 漏洞的经历。以Apache ActiveMQ 的 CVE-2023–46604 为特例&#xff0c;重点介绍如何发现类似此类的漏洞&#xff0c;让我们开始吧。 快速发现过程 在‘侦察’阶段&#xff0c;白帽小哥会保持每周更新一次目标…

1940java swing零售库存管理系统myeclipse开发Mysql数据库CS结构java编程

一、源码特点 java swing 零售库存管理系统 是一套完善的窗体设计系统&#xff0c;对理解SWING java 编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;&#xff0c;系统主要采用C/S模式开发。 应用技术&#xff1a;javamysql 开发工具&#xff1a;…

适合技术小白学习的项目1863java在线视频网站系统 Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 java在线视频网站系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助采用了java设计&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统采用web模式&#xff0c;系统主要采用B/S模式开发。 开发环境为TOMCAT7.0,Myeclipse8.5开发…

数据库、数据表的基本操作

1.数据库的基本操作 &#xff08;1&#xff09;创建数据库 &#xff08;2&#xff09;删除数据库 &#xff08;3&#xff09;将数据库的字符集修改为gbk gbk是汉字内码扩展规范&#xff0c;是GB2312和GB13000的扩展&#xff0c;主要用于简体中文。 &#xff08;4&#xff09;…

LabVIEW在高校电力电子实验中的应用

概述&#xff1a;本文介绍了如何利用LabVIEW优化高校电力电子实验&#xff0c;通过图形化编程实现参数调节、实时数据监控与存储&#xff0c;并与Simulink联动&#xff0c;提高实验效率和数据处理能力。 需求背景高校实验室在进行电机拖动和电力电子实验时&#xff0c;通常使用…

前端框架安全防范

前端框架安全防范 在现代Web开发中&#xff0c;前端框架如Angular和React已经成为构建复杂单页面应用&#xff08;SPA&#xff09;的主流工具。然而&#xff0c;随着应用复杂度的增加&#xff0c;安全问题也变得越来越重要。本文将介绍如何在使用Angular和React框架时&#xf…

施耐德 BAS PLC 基本操作指南

CPU 型号 项目使用的 PLC 型号为&#xff1a;施耐德昆腾 Quantum 140 CPU 67160 P266 CPU &#xff0c;支持热备冗余&#xff0c;内部存储 1024K&#xff0c;支持 2 个 PCMCIA 扩展卡槽CPU 模块自带接口&#xff1a;MB 串口接口、MB 串口接口、USB 接口、以太网接口&#xff…

【HarmonyOS】List组件多层对象嵌套ForEach渲染更新的处理

【HarmonyOS】List组件多层对象嵌套ForEach渲染更新的处理 问题背景&#xff1a; 在鸿蒙中UI更新渲染的机制&#xff0c;与传统的Android IOS应用开发相比。开发会简单许多&#xff0c;开发效率提升显著。 一般传统应用开发的流程处理分为三步&#xff1a;1.画UI&#xff0c;…

TiDB-从0到1-分布式存储

TiDB从0到1系列 TiDB-从0到1-体系结构TiDB-从0到1-分布式存储TiDB-从0到1-分布式事务TiDB-从0到1-MVCC 一、TiDB-DML语句执行流程&#xff08;增删改&#xff09; DML流程概要 1、协议验证 用户连接到TiDB Server后首先工作的是Protocol Layer模块&#xff0c;该模块会对用…

mysql表字段超过多少影响性能 mysql表多少效率会下降

一直有传言说&#xff0c;MySQL 表的数据只要超过 2000 万行&#xff0c;其性能就会下降。而本文作者用实验分析证明&#xff1a;至少在 2023 年&#xff0c;这已不再是 MySQL 表的有效软限制。 传言 互联网上有一则传言说&#xff0c;我们应该避免单个 MySQL 表中的数据超过 …

内网渗透-在HTTP协议层面绕过WAF

进入正题&#xff0c;随着安全意思增强&#xff0c;各企业对自己的网站也更加注重安全性。但很多web应用因为老旧&#xff0c;或贪图方便想以最小代价保证应用安全&#xff0c;就只仅仅给服务器安装waf。 本次从协议层面绕过waf实验用sql注入演示&#xff0c;但不限于实际应用…

[数据集][目标检测]轮胎检测数据集VOC+YOLO格式439张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;439 标注数量(xml文件个数)&#xff1a;439 标注数量(txt文件个数)&#xff1a;439 标注类别…

颠仆流离学二叉树2 (Java篇)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

泛型知识汇总

演示代码&#xff1a; package exercise;import java.util.Arrays;public class MyArrayList<E> {Object[] obj new Object[10];int size;public boolean add(E e) {obj[size] e;size;return true;}public E get(int index) {return (E) obj[index];}//没有这个函数&a…

现代信号处理12_谱估计的4种方法(CSDN_20240602)

Slepian Spectral Estimator(1950) 做谱估计的目标是尽可能看清楚信号功率谱在某一个频率上的情况&#xff0c;假设我们想了解零频时的分布&#xff0c;最理想的情况是滤波器的传递函数H(ω) 是一个冲激函数&#xff0c;这样就没有旁瓣&#xff0c;也就没有泄漏&#xff1b;其次…

【OpenHarmony】TypeScript 语法 ③ ( 条件语句 | if else 语句 | switch case 语句 )

文章目录 一、条件语句1、if else 语句2、switch case 语句 参考文档 : <HarmonyOS第一课>ArkTS开发语言介绍 一、条件语句 1、if else 语句 TypeScript 中的 if 语句 / if else 语句 用法 , 与 JavaScript 语言中的 if 语句 / if else 语句 语法 基本相同 ; if else 语…

项目质量管理

目录 1.概述 2.三个关键过程 2.1.规划质量管理&#xff08;Plan Quality Management&#xff09; 2.2.管理质量&#xff08;Manage Quality&#xff09; 2.3.控制质量&#xff08;Control Quality&#xff09; 3.应用场景 3.1.十个应用场景 3.2.产品设计与开发 4.小结…

使用PyCharm 开发工具创建工程

一. 简介 前面学习了 安装 python解释器。如何安装python的一种开发工具 PyCharm。 本文来简单学习一下&#xff0c;如何使用 PyCharm 开发工具创建一个简单的 python工程。 二. PyCharm 开发工具创建一个工程 1. 首先&#xff0c;首先打开PyCharm 开发工具。选择 创建一…

Docker部署SiYuan笔记-Unraid

使用unraid的docker部署SiYuan笔记&#xff0c;简单记录 笔记说明 Siyuan笔记是一款基于markdown语法的笔记工具&#xff0c;具有活跃的社区和多设备支持。大部分功能都是免费&#xff0c;源代码开源&#xff0c;支持插件安装&#xff0c;具有很不错的使用体验。 Docker地址&a…