在本文中, 咱們主要介紹如何分析遞歸算法程序中的時間複雜度。.
在一個遞歸程序中, 它的時間複雜度 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/