深入解析JavaScript中的递归和堆栈

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

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

​ 

✨ 前言

        递归是一种常见的编程技巧,它意味着一个函数在其函数体内调用自身。递归函数在遇到基线条件时会停止调用自身,否则会陷入无限循环。

        在JavaScript中,由于其单线程运行时的特点,递归函数的内部调用会被添加到调用栈(call stack)中。调用栈以LIFO(后进先出)的顺序运行,当函数执行完毕后会从栈中弹出。

✨ 正文

一、原理

递归函数的工作原理:

  1. 定义一个递归函数,指定输入参数和返回值。
  2. 在函数内部,添加一个条件来检查参数是否满足基线条件(停止递归的条件)。
  3. 如果参数满足基线条件,则直接返回结果。
  4. 如果参数不满足基线条件,则调用自身函数传入更新后的参数,并返回函数调用的结果。

一个计算 factorial 的示例:

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

factorial(5); // 120

        在这个例子中,如果n等于1,则直接返回1。否则将调用自身函数factorial,传入n-1,并将返回值与n相乘,这种递归调用会一直进行直到n等于1。

        随着递归深度的增大,调用栈也会不断地增长,函数调用会被添加到栈的顶部。当基线条件满足返回结果时,栈顶的函数会开始出栈,递归树才会逐步收敛。

        如果递归函数无法满足基线条件,调用栈会持续增长直到抛出"Maximum call stack size exceeded"错误。所以编写递归时需要注意控制最大深度。

        递归函数的执行过程可以通过栈的Push和Pop来模拟,更直观地展示递归函数的运行机制。

以计算factorial(5)为例:

  1. 当调用factorial(5)时,由于n不等于1,所以会进行递归调用factorial(4)。此时发生了一次函数调用,相当于栈Push了一次,栈顶函数是factorial(5)。
  2. factorial(4)函数执行,n不等于1,继续递归调用factorial(3),又Push了一次栈,栈顶函数是factorial(4)。
  3. 重复这个逻辑,直到调用factorial(1),这时候n等于1,满足基线条件,可以返回1,不再递归调用。
  4. 此时栈顶函数是factorial(2),在其内部执行return 2 * factorial(1),由于factorial(1)已经返回1,所以可以得到返回值2。
  5. factorial(2)执行完毕,将从栈中Pop出,栈顶变成factorial(3)。
  6. factorial(3)得到返回值3 * factorial(2),即3 * 2= 6,执行完毕后Pop。
  7. 依次类推,factorial(4)返回值是4 * 6 = 24,factorial(5)返回5 * 24 = 120。
  8. 最终factorial(5)也会从栈中Pop出,递归调用结束。

        通过这个过程,递归函数可以看作是不断地栈Push和Pop的过程,最后递归树不断展开收敛,得到最终结果。

二、深入解析

  • 递归函数的两个要素
  1. 递归函数必须有一个终止条件,否则会出现无限递归导致栈溢出。
  2. 递归函数在非终止条件情况下必须调用自身,并传入不同的参数,以推进向终止条件的执行。
  • 递归函数与尾递归优化
  1. 普通递归在函数调用后还需要执行其它操作才能返回,所以无法进行尾递归优化。
  2. 尾递归是指递归调用作为函数的最后一步操作,这种可以进行优化,改写成迭代的形式。
  • 递归函数的空间复杂度
  1. 递归函数空间复杂度受最大递归深度影响。每次递归调用会增加栈帧,空间复杂度为O(n)。
  2. 可以通过设置最大递归深度来控制空间 usage。
  • 递归函数的时间复杂度
  1. 递归函数的时间复杂度和问题的递归结构相关。如二分查找为O(logn)。
  2. 尾递归优化后可以将空间复杂度降低为O(1)。
  • 递归函数优化
  1. 尾递归优化可以减少空间复杂度,使用循环来改写递归。
  2. 空间复杂度也可以通过限制最大递归深度来控制。
  3. 利用记忆化搜索可以避免重复计算,降低时间复杂度。
  • 递归函数的适用场景
  1. 可以用递归求解分治问题,如mergesort、快速排序、二分查找等。
  2. 也可以解决树形结构的问题,如遍历DOM树、文件目录等。
  3. 动态规划问题也可以用递归求解,并配合记忆化搜索优化。

 

✨ 结语

        通过上面的详细解读,我们可以看到递归是一种功能强大且优雅的编程技巧。正确使用递归可以写出简洁易懂的代码,提高开发效率。但是我们也要注意控制递归的最大深度,避免造成内存溢出。

        当遇到可以递归解决的问题时,要思考清楚递归的终止条件和每一层的计算过程。若问题可用迭代实现,也要考虑使用尾递归优化递归算法的性能。

        另外,递归可配合动态规划、分治法等算法范式发挥更大威力。掌握递归的精髓,可以让我们写出高效可靠的程序,解决更广泛的问题。

你可能感兴趣的:(JavaScript保姆级教程,javascript,开发语言,ecmascript,前端)