LeetCode递归 -2(Recursion) 培训专题 讲解文章翻译 (附链接) (2019-04-09 15:50)
递归 - 空间复杂度
在本文中, 我们将讨论如何分析递归算法的空间复杂度.
在计算递归算法的空间复杂度时,最需要考虑的两个部分就是: 递归相关空间 (recursion related space)和非递归相关空间(non-recursion related space).
递归相关空间
递归相关空间指的是递归直接产生的内存开销,也就是在递归调用过程中开辟的堆栈内存空间。为了完成一个典型的函数调用,系统中在开辟内存空间时,会存放一下3个重要的数据信息:
- 调用函数的返回地址. 一旦程序调用结束,需要这个地址返回, 即程序调用结束进行下一步的地址;
- 该调用函数所需要的参数;
- 调用函数的内部变量.
尽管堆栈是在递归调用时产生的最小消耗. 旦只要调用过程结束,这些空间也会被释放掉。
对于递归算法,函数的调用将会持续产生调用链,直至调用到基本型 (base case) (亦称作. bottom case).这意味着每个函数的调用也是逐步累积的.
对于递归算法, 如果没有其他的内存消耗, 这个算法的空间上限就是该递归调用直接产生的内存开销。
还是拿练习 printReverse说事,我们每次调用只是打印了一个字符,所以在递归调用之外我们没有用掉其他的内存开销, .对于每一次的递归调用, 假设需要一个固定常量的空间. 那么根据前面的分析,他会调用 n 次函数链, n 就是输入的字串长度. 所以该算法的空间复杂度即为 {\mathcal{O}(n)}O(n).
为了更好地说明, 对于递归调用序列 f(x1) -> f(x2) -> f(x3) 的执行步骤序列以及堆栈的布局如下图所示:
f(x1) 将会被分配一定额外的空间以便调用f(x2). 对于 f(x2)调用f(x3).时候的内存操作也是同样类型的情况,直到 f(x3),的调用到达了基本型, 当到达 f(x3).时,不在分配额外空间。
这种调用机制导致有时分配给堆栈的空间有时超出内存限制,会导致一种程序崩溃的情况,叫做栈溢出(stack overflow),所以当设计采用这种递归调用机制的程序时,首要考虑的就是输入量是否会导致溢出情况的发生。
非递归相关空间
显而易见, 所谓非递归相关空间即与递归调用链过程中不直接相关的内存开销, 比如为全局变量所分配的空间(一般情况下都存放在堆heap中).
不论是否是递归程序,在执行下一个子程序之前总需要一个全局变量来存放输入的数据。 同样对于调用函数的返回值也需要开辟空间来存放.后者的一种情况就是我们前面讨论过的 记忆化 处理方式. 例如在处理斐波那契额数列的记忆化递归算法中, 我们使用一个map来存储每次递归调用返回的结果值. 因此, 在计算该算法的空间复杂度的时候, 我们同时也需要考虑记忆化操作所带来的空间成本.