详解JS递归和尾递归

310 阅读3分钟

递归是编程中的一种常见技术,它允许一个函数调用自身来解决问题。尾递归是递归的一种特殊形式,它在某些编程语言中可以优化递归调用,减少栈空间的使用。

递归

递归的概念

递归是指一个函数在其定义中调用自身。递归通常用于解决可以分解为相似子问题的问题。递归函数通常包含两个部分:

  1. 基准情况(Base Case):递归终止的条件,防止无限递归。
  2. 递归情况(Recursive Case):函数调用自身来解决子问题。

使用场景

递归适用于以下场景:

  • 树形结构的遍历:如 DOM 树、文件系统树等。
  • 分治算法:如快速排序、归并排序等。
  • 数学问题:如阶乘、斐波那契数列等。

递归示例:计算阶乘

function factorial(n) {
  // 基准情况
  if (n === 0) {
    return 1;
  }
  // 递归情况
  return n * factorial(n - 1);
}

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

解释

  • 基准情况:当 n 为 0 时,返回 1。
  • 递归情况:函数调用自身 factorial(n - 1),并将结果乘以 n

尾递归

尾递归的概念

尾递归是递归的一种特殊形式,其中递归调用是函数中的最后一个操作。尾递归可以优化递归调用,减少栈空间的使用。在支持尾递归优化的编程语言中,尾递归调用不会增加调用栈的深度,从而避免栈溢出。

使用场景

尾递归适用于以下场景:

  • 需要优化递归调用的场景:如深度较大的递归调用,避免栈溢出。
  • 数学问题:如阶乘、斐波那契数列等。

尾递归示例:计算阶乘

function factorial(n, acc = 1) {
  // 基准情况
  if (n === 0) {
    return acc;
  }
  // 尾递归情况
  return factorial(n - 1, n * acc);
}

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

解释

  • 基准情况:当 n 为 0 时,返回累积结果 acc
  • 尾递归情况:函数调用自身 factorial(n - 1, n * acc),并将累积结果传递给下一个递归调用。

递归与尾递归的区别

  • 递归:函数在调用自身时,可能在递归调用后还有其他操作,导致每次递归调用都会增加调用栈的深度。
  • 尾递归:函数在调用自身时,递归调用是最后一个操作,不会增加调用栈的深度。在支持尾递归优化的编程语言中,尾递归调用可以优化为迭代,减少栈空间的使用。

详细代码示例

示例 1:递归遍历 DOM 树

function traverseDOM(node) {
  console.log(node.tagName); // 输出节点的标签名
  // 遍历子节点
  for (let child of node.children) {
    traverseDOM(child);
  }
}

// 测试
traverseDOM(document.body);

示例 2:尾递归计算斐波那契数列

function fibonacci(n, a = 0, b = 1) {
  // 基准情况
  if (n === 0) {
    return a;
  }
  // 尾递归情况
  return fibonacci(n - 1, b, a + b);
}

// 测试
console.log(fibonacci(10)); // 输出 55

解释

  • 递归遍历 DOM 树:函数 traverseDOM 递归遍历 DOM 树,输出每个节点的标签名。

  • 尾递归计算斐波那契数列:函数 fibonacci 使用尾递归计算斐波那契数列,避免栈溢出。

  • 递归:适用于解决可以分解为相似子问题的问题,如树形结构的遍历、分治算法、数学问题等。递归函数包含基准情况和递归情况。

  • 尾递归:递归的一种特殊形式,递归调用是函数中的最后一个操作。在支持尾递归优化的编程语言中,尾递归调用可以优化为迭代,减少栈空间的使用。适用于需要优化递归调用的场景,如深度较大的递归调用。

通过理解递归和尾递归的概念、使用场景和代码示例,可以更好地应用这些技术来解决实际问题。