在本文中, 咱们主要介绍如何分析递归算法程序中的时间复杂度。.
在一个递归程序中, 它的时间复杂度 O(T) 通常来讲就是他总共递归调用的次数 (定义为 R) 以及每次调用时所花费的耗时 (定义为 O(s)) ,这样咱们就能够得出:
(T) = R * O(T) = R∗O(s)
下面让咱们来看几个栗子:
线性的栗子
printReverse(str) = printReverse(str[1...n]) + print(str[0])
其中 str[1...n] 是输入的字串 str 去除了首字母str[0]的切分子串, .
显而易见,这个算法会连续调用 n 次, 这个 n 也就该输入字串的长度. 在每次递归的最后, 咱们只打印首字母, 所以该算法每次调用递归所耗费的时间为常量, 即为 {\mathcal{O}(1)}O(1).
把次数和每次耗时进行合计,该递归程序 printReverse(str) 的耗时即为 (printReverse) = n * O(1) = O(printReverse)=n∗O(1)=O(n).
执行树
对于递归函数, 像上面的线性化递归调用的栗子实际上是不多的,更多的是非线性的. 例如, 以前章节咱们讨论的
Fibonacci number ,它的递归关系就定义为更复杂的 f(n) = f(n-1) + f(n-2). 乍看之下, 很难一会儿去计算出斐波那契函数的递归调用次数 -_-.
在这个例子里, 咱们最好使用 execution tree 这个工具能够用来直观地表示递归程序的执行流. 树中的每个节点都表示每次对应的递归程序调用. 所以,树中的节点总数与整个递归过程调用的总数相对应。
递归函数的执行树会造成一个 n-ary tree, 其中n 就是这个程序执行下来递归调用的次数. 例如, 斐波那契函数的执行流就是一个二叉树, 以下图所示就是计算 f(4) 的流程树:
在一个 n 层的满二叉树, 全部节点数总和应该是
2^n - 1
. 所以, 对于递归程序 f(n) 总调用次数的上限也应该是
{2^n -1}
. 因此, 咱们得出了递归程序 f(n) 的时间复杂度即为
{\mathcal{O}(2^n)}
记忆化
在前面的章节中, 咱们讨论过用来优化递归算法时间复杂度的记忆化方法. 经过存储和重复使用中间变量, 记忆化可以极大地下降递归程序的调用次数, 换个说法就是减小执行树中的递归调用分支. 在分析试用了记忆化的递归调用程序的时间复杂度时,千万要记得考虑这种(分支减小的)状况。.
让咱们从新再回看前面斐波那契额数列的栗子. 使用记忆化方法的话,咱们每次都将斐波那契额数列在 n.节点下的存储, 因而咱们能够确保对于每一个节点计算须要的递归调用只须要一次. 并且咱们知道斐波那契额数列的递归关系是每一个 f(n) 都依赖前一个 n-1 的结果. 最终使得计算 f(n) 只调用 n-1 次 以前已经计算好的结果便可.
如今, 咱们能够很轻易的经过前面介绍的公式 O(1)∗n=O(n) .来计算斐波那契额数列函数的时间复杂度。记忆化不单单优化算法的时间复杂度,一样也简化了对于时间复杂度的计算。
在下一篇文章中, 咱们将讨论如何估算递归程序的空间复杂度.
原文地址:https://leetcode.com/explore/learn/card/recursion-i/256/complexity-analysis/1669/