写点什么

编程技巧:什么是 JavaScript 递归

作者:高端章鱼哥
  • 2024-05-13
    福建
  • 本文字数:2487 字

    阅读完需:约 8 分钟

编程技巧:什么是JavaScript递归

什么是递归

程序调用自身的编程技巧称为递归(recursion)

递归的基本思想是将一个复杂的问题分解成更小、更易于管理的子问题,这些子问题与原始问题相似,但规模更小。

递归的要素

  • 基本情况(Base Case):这是递归终止的条件,也就是说,当问题足够小,可以直接解决而不需要进一步递归时,就会返回一个直接的答案。

  • 递归步骤(Recursive Step):在这一步中,问题被分解成更小的子问题,并且递归地解决这些子问题。每个递归调用都向基本情况更进一步。

递归与循环的比较

递归和循环是实现重复操作的两种基本方法。它们在不同的场景下各有优势。

场景

  • 递归适合解决可以分解为逐渐缩小的子问题的问题。

  • 循环通常用于需要重复执行固定次数的操作,或者在满足特定条件之前重复执行。

性能

  • 递归由于每次调用都会占用新的栈空间,可能会导致栈溢出,特别是在深度递归时。此外,递归的执行效率通常低于循环,因为它涉及更多的函数调用开销。

  • 循环通常更高效,因为它避免了函数调用的开销,并且可以更直接地控制循环的次数。

限制

  • 递归需要有明确的终止条件,否则会导致无限递归。

  • 循环也需要有明确的终止条件,否则也可能导致无限循环。

阶乘

递归实现阶乘:

基本情况:基本情况就是结束条件,这可以有很多,比如:n = 1 时,返回 1

n = 2 时,返回 2 (1 * 2)

n = 3 时,返回 6 (1 * 2 * 3)

我们使用最简单的结束条件:n = 1 时,返回 1

递归步骤:每次递归调用时,都会将之前的结果乘以当前数字,然后返回结果。

实现:

function factorial(n) {  if (n == 1) return n;  return n * factorial(n - 1);}
console.log(factorial(5)); // 5 * 4 * 3 * 2 * 1 = 120
复制代码

通过下图,可以看出这个递归函数的实现原理:


循环实现阶乘:

function factorial(n) {  let result = 1; 
for (let i = 2; i <= n; i++) { result *= i; }
return result;}
console.log(factorial(5)); // 输出: 120
复制代码

斐波那契数列

斐波那契数列是一个特殊的数列,其中每个数都是前两个数的和。

1, 1, 2, 3, 5, 8, 13, 21, 34, ...
复制代码

下来我们实现一个函数,返回斐波那契数列的第 n 个数。

斐波那契数列的递归实现:

基本情况:

如果 n 为 0 或 1 ,直接返回 n

递归步骤:

每次递归调用都返回前两个数的和。

实现:

function fibonacci(n) {  if (n === 0) return 0;  if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);}
// 计算第6项斐波那契数console.log(fibonacci(6)); // 输出: 8
复制代码

斐波那契数列的循环实现:

function fibonacci(n) {  // 初始化数组,包含前两项  let fib = [1, 1];
for (let i = 2; i < n; i++) { // 计算当前项,即前两项的和 fib[i] = fib[i - 1] + fib[i - 2]; }
return fib[n - 1];}
// 计算第6项斐波那契数console.log(fibonacci(6)); // 输出: 8
复制代码

尾递归

我们看这个斐波那契数列的递归实现,他的函数调用次数要比循环次数多很多。我们打印出来看下。

let count = 0;function fibonacci(n) {  count++;  if (n === 0) return 0;  if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);}
// 计算第6项斐波那契数console.log(fibonacci(6)); // 输出: 8console.log(`调用 ${count} 次`); // 输出: 调用 25 次
复制代码

函数执行一次,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。我们这个递归函数,创建很多执行上下文压入执行上下文栈。

递归次数过多还会导致栈溢出。


在 chrome 浏览器的 Sources 面板中,我们通过 Call Stack 可以看到执行上下文的函数调用栈的具体情况


下来我们开始使用尾递归优化这个递归函数。

尾递归是指递归函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。这样就可以避免创建新的栈帧。

我们直接将上一次的结果,作为参数传递给下一次的函数调用。

function fibonacci(n, ac1 = 1, ac2 = 1) {  if (n == 1 || n == 2) {    return ac2;  }
return fibonacci(n - 1, ac2, ac1 + ac2);}
// 计算第6项斐波那契数console.log(fibonacci(6)); // 输出: 8
复制代码

注意:上面的 fibonacci 函数虽然已经使用了尾递归优化,但还是可能出现栈溢出。

因为 JavaScript 引擎并不总是能够优化尾递归调用以避免栈溢出。这是因为尾递归优化( Tail Call Optimization , TCO )并不是 JavaScript 语言规范的一部分,也不是所有 JavaScript 引擎都实现了这一优化。

在没有 TCO 的环境中,每次递归调用仍然会占用新的栈空间,fibonacci 函数的每次调用都需要保留其执行上下文(包括参数和局部变量),直到该调用的返回值被使用。随着 n 的增加,递归调用的深度也会增加,这最终可能导致栈溢出。

递归应用的业务场景

数组转树

比如在做权限控制、级联选择组件的时候,后端返回的数据可能是数组,前端需要将其转换为树形结构。这时可以使用递归来解决。

JavaScript 实现扁平数组与树结构的相互转换

递归组件

在 Vue 中,递归组件是一种可以调用自身作为其子组件的组件。递归组件非常适合用来展示嵌套的或者树状的数据结构,例如文件系统、组织结构图、菜单列表等。

在做侧边栏可能有多个层级的时候,可以使用递归组件。

在 vue-element-admin 项目中的 SidebarItem 组件 就是使用了递归组件。

总结

使用递归解决问题,重点是如果把问题分解为更小的相同问题,确定递归终止条件,然后递归调用自身。使用递归要注意栈溢出问题,可以考虑使用尾递归优化。

前端的世界总是在不断变化,作为开发者,我们需要保持好奇心和学习热情,不断探索新的技术,只有这样,我们才能在这个快速发展的时代中立于不败之地。介绍一款程序员都应该知道的软件JNPF快速开发平台,平台非常好用,是功能的集大成者,任何信息化系统都可以基于它开发出来。

这是一个基于 Java Boot/.Net Core 构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;集成了代码生成器,支持前后端业务代码生成,实现快速开发,提升工作效率;框架集成了表单、报表、图表、大屏等各种常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3。如果你有闲暇时间,可以做个知识拓展。

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
编程技巧:什么是JavaScript递归_高端章鱼哥_InfoQ写作社区