编程技巧:什么是JavaScript递归

什么是递归

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

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

递归的要素

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

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

递归与循环的比较

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

场景

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

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

性能

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

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

限制

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

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

阶乘

递归实现阶乘:

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

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

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

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

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

实现:

scss 复制代码
function factorial(n) {
  if (n == 1) return n;
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 5 * 4 * 3 * 2 * 1 = 120

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

循环实现阶乘:

ini 复制代码
function factorial(n) {
  let result = 1; 

  for (let i = 2; i <= n; i++) {
    result *= i;
  }

  return result;
}

console.log(factorial(5)); // 输出: 120

斐波那契数列

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

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

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

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

基本情况:

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

递归步骤:

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

实现:

scss 复制代码
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

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

ini 复制代码
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

尾递归

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

ini 复制代码
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)); // 输出: 8
console.log(`调用 ${count} 次`); // 输出: 调用 25 次

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

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

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

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

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

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

scss 复制代码
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。如果你有闲暇时间,可以做个知识拓展。

相关推荐
苹果酱056714 分钟前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件
掐指一算乀缺钱34 分钟前
SpringBoot 数据库表结构文档生成
java·数据库·spring boot·后端·spring
晚睡早起₍˄·͈༝·͈˄*₎◞ ̑̑39 分钟前
苍穹外卖学习笔记(七)
java·windows·笔记·学习·mybatis
就这个java爽!1 小时前
JAVA网络编程【基于TCP和UDP协议】超详细!!!
java·开发语言·网络·tcp/ip·udp·eclipse·idea
一叶飘零_sweeeet1 小时前
为什么 Feign 要用 HTTP 而不是 RPC?
java·网络协议·http·spring cloud·rpc·feign
天下无贼!1 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr1 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林1 小时前
npm发布插件超级简单版
前端·npm·node.js
懒洋洋大魔王1 小时前
7.Java高级编程 多线程
java·开发语言·jvm
茶馆大橘1 小时前
【黑马点评】已解决java.lang.NullPointerException异常
java·开发语言