遞歸深度就是遞歸函數在內存中,同時存在的最大次數。
例以下面這段求階乘的代碼:
Java:html
int factorial(int n) { if (n == 1) { return 1; } return factorial(n - 1) * n; }
Python:算法
def factorial(n): if n == 1: return 1 return factorial(n-1) * n
C++:函數
int factorial(int n) { if (n == 1) { return 1; } return factorial(n - 1) * n; }
當n=100
時,遞歸深度就是100。通常來講,咱們更關心遞歸深度的數量級,在該階乘函數中遞歸深度是O(n)O(n)O(n),而在二分查找中,遞歸深度是O(log(n))O(log(n))O(log(n))。在後面的教程中,咱們還會學到基於遞歸的快速排序、歸併排序、以及平衡二叉樹的遍歷,這些的遞歸深度都是(O(log(n))(O(log(n))(O(log(n))。注意,此處說的是遞歸深度,而並不是時間複雜度。優化
首先,函數自己也是在內存中佔空間的,主要用於存儲傳遞的參數,以及調用代碼的返回地址。
函數的調用,會在內存的棧空間中開闢新空間,來存放子函數。遞歸函數更是會不斷佔用棧空間,例如該階乘函數,展開到最後n=1
時,內存中會存在factorial(100), factorial(99), factorial(98) ... factorial(1)
這些函數,它們從棧底向棧頂方向不斷擴展。
當遞歸過深時,棧空間會被耗盡,這時就沒法開闢新的函數,會報出stack overflow
這樣的錯誤。
因此,在考慮空間複雜度時,遞歸函數的深度也是要考慮進去的。spa
Follow up:
尾遞歸:若遞歸函數中,遞歸調用是整個函數體中最後的語句,且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸。(上例factorial函數知足前者,但不知足後者,故不是尾遞歸函數)
尾遞歸函數的特色是:在遞歸展開後該函數再也不作任何操做,這意味着該函數能夠不等子函數執行完,本身直接銷燬,這樣就再也不佔用內存。一個遞歸深度O(n)O(n)O(n)的尾遞歸函數,能夠作到只佔用O(1)O(1)O(1)空間。這極大的優化了棧空間的利用。
但要注意,這種內存優化是由編譯器決定是否要採起的,不過大多數現代的編譯器會利用這種特色自動生成優化的代碼。在實際工做當中,儘可能寫尾遞歸函數,是很好的習慣。
而在算法題當中,計算空間複雜度時,建議仍是老老實實地算空間複雜度了,尾遞歸這種優化提一下也是能夠,但別太在乎。code