JavaScript递归技巧的前世今生:深入解析递归及其与堆栈的关系

个人主页:《爱蹦跶的大A阿》

当前正在更新专栏:《VUE》 、《JavaScript保姆级教程》、《krpano》

​ 

✨ 前言

        递归作为一种能够用简洁的方式定义复杂对象的编程技巧,在计算机科学中被广泛应用。它借助系统堆栈的先入后出结构,将大问题拆分为小问题来解决,对于二叉树、组合问题等都是非常高效的解决方案。

        但是递归也有其局限性,它占用堆栈空间,存在最大调用层数限制。为了发挥递归技巧的优势,同时规避其缺点,我们需要深入理解递归的原理及其与堆栈的关系,掌握改写递归的技巧。

        本文将从递归和堆栈的基本概念出发,剖析两者之间的内在联系,分析递归技巧的优缺点,给出其典型应用场景,并通过示例讲解递归代码的实现思路。最后,将提供改写递归的实用技巧,帮助读者更好地运用这一编程工具。

✨ 正文

一、递归和堆栈的概念

递归是一种编程技巧,它会直接或间接地调用函数自身来解决问题,就像一面镜子可以不断产生镜中的镜像一样。递归会把大问题分解为小问题来解决,小问题的解决方案又依赖于大问题的解决。

堆栈是计算机管理函数调用的一种数据结构,它采用先进后出的方式存储信息。当一个函数开始执行时,会被添加到堆栈的顶部;当一个函数执行结束,就会被堆栈删除。递归函数的实现就是利用了这种堆栈结构。

二、递归的两个必要条件

一个递归函数必须同时满足以下两个条件:

  1. 必须有一个基准条件(base case)。当满足这个条件时,递归不再继续。
  2. 递归条件(recursive case)。这一步把原问题分解成更小的子问题。

只要同时满足这两个条件,就可以使用递归来解决问题。

三、递归和堆栈的关系

递归函数在执行时会保存每一层的函数调用现场信息,这些信息被保存在系统的运行时堆栈中。可以把递归看成是不断把函数调用现场信息压入堆栈,并在满足基准条件时逐层将堆栈弹出并恢复函数的执行现场。

四、递归的优缺点

优点:

  1. 递归容易理解和实现。
  2. 递归可以用于解决分治法的问题。
  3. 递归允许程序以相对简洁的方式定义复杂对象。

缺点:

  1. 递归函数调用开销较大,占用堆栈空间。
  2. 递归超过最大调用层数会发生“栈溢出”错误。
  3. 递归函数调试困难。

五、递归的常见场景

  • 各种数学问题,如阶乘、斐波那契数列等。

(1)计算阶乘

function factorial(n) {
  if (n == 1) return 1;
  return n * factorial(n - 1);
}

(2)斐波那契数列 

function fib(n) {
  if (n <= 2) return 1;
  return fib(n-1) + fib(n-2);
}
  • 树和图的遍历。

以二叉树前序遍历为例:

function preorder(root) {
  if (!root) return;  
  console.log(root.val);
  preorder(root.left);
  preorder(root.right);
}
  • 动态规划灌水法。

比如实现DFS深度优先遍历:

function dfs(graph, current, visited) {
  visited.add(current);
  for (let neighbor of graph[current]) {
    if (!visited.has(neighbor)) {
      dfs(graph, neighbor, visited);
    }
  }
}
  • 逆序、分治、回溯等算法。

例如归并排序:

function mergeSort(arr) {
  if (arr.length === 1) {
    return arr;
  }

  const mid = Math.floor(arr.length / 2);
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);

  return merge(mergeSort(left), mergeSort(right)); 
}

六、递归实现的案例

以计算阶乘为例,递归代码如下:

function factorial(n) {
  if (n === 1) {
    return 1
  }
  return n * factorial(n - 1)
}

factorial(5) // 120

满足基准条件(n === 1)时返回1,否则不断调用自身计算n*(n-1)的阶乘。

七、递归的改写技巧

  1. 引入缓存,避免重复计算。
  2. 尝试用循环重写递归。
  3. 将高开销的递归改为低开销的迭代形式。
  4. 在尾递归优化的语言中使用尾递归优化。

 

✨ 结语

        理解递归技巧的本质在于掌握递归与堆栈的关系。递归允许程序以简洁的方式定义复杂对象,是计算机科学中一把双刃剑。运用得当,可以事半功倍;使用不当,后果不堪设想。

        本文全面解析了递归及其与堆栈的千丝万缕的联系,剖析了递归技巧的优劣势,并提供了改写递归的有效方法。希望这些知识可以帮助读者在运用递归技巧时既发挥它的高效优势,又规避其潜在缺陷。 

你可能感兴趣的:(JavaScript保姆级教程,前端)