一般算法的空間複雜度相信大家已經都掌握了
那麼大家想一想遞歸算法的空間複雜度應該怎麼分析呢。
我這裏用一個到簡單的題目來舉例
題目:求第n的斐波那契數
相信使用遞歸算法來求斐波那契數,大家應該再熟悉不過了
代碼如下:
int fibonacci(int i) {
if(i <= 0) return 0;
if(i == 1) return 1;
return fibonacci(i-1) + fibonacci(i-2);
}
可以看出代碼非常簡短,這時同學們就會有一種這種簡短的代碼,會有一種這時“高效”算法的錯覺
我們來看這個遞歸算法的時間複雜度是多少
我在講遞歸算法時間複雜度的文章裏講過 一場面試,帶你徹底掌握遞歸算法的時間複雜度
求遞歸時間複雜度就是看它 每次遞歸進行了什麼操作和遞歸了多少次
可以上面代碼中每次遞歸都是O(1)的操作
再來看遞歸了多少次,
這裏將就第n的斐波那契數,n爲5爲輸入的遞歸過程 抽象成一顆遞歸樹如下:
在這課二叉樹中,我們可以看出每一個節點都是一次遞歸,每個節點都有兩個子節點
那麼我們這棵樹有多少個節點
一個高度爲k的二叉樹最多可以有 2^k - 1個節點
所以我們該遞歸算法的時間複雜度爲 O(2^n) ,這個複雜度是非常大的
隨着n的增大,耗時是指數上升的。大家可以做一個實驗,看看約着n的增大,這個遞歸求斐波那契的代碼的耗時。
所以這種求斐波那契數的算法看似簡潔,其實時間複雜度非常高,一般不推薦這樣來寫。
說完了時間複雜度,那在看看如何求遞歸算法的空間複雜度呢,這裏我給大家一個公式
遞歸算法的空間複雜度 = 每次遞歸的空間複雜度 * 遞歸深度
首先看每次遞歸的空間複雜度,因爲這個算法中我們可以看出每次遞歸所需要的空間大小都是一樣的
而且就算是第N次遞歸,每次遞歸所需的棧空間也是一樣的。
所以每次遞歸中需要的空間是一個常量,並不會隨着n的變化而變化,每次遞歸的空間複雜度就是O(1)
求遞歸的空間複雜度,那麼爲什麼要看遞歸的深度呢
每次遞歸所需的空間都被壓到調用棧裏(這是內存管理裏面的數據結構,和我們算法裏的棧原理是一樣的)
看遞歸算法的空間消耗,就是要看調用棧所佔用的大小
一次遞歸結束,這個棧就是就是把本次遞歸的數據彈出去。所以這個棧最大的長度就是 遞歸的深度。
我們在用這顆二叉樹來舉例
通過模擬遞歸的調用過程,來看一下調用棧大小的變化,二叉樹的前序遍歷就是遞歸的調用過程
如圖所示,求第n的斐波那契數,n爲5爲輸入的遞歸過程
遞歸過程
調用棧深度
通過這個舉例,可以看出 遞歸第n個斐波那契數的話,遞歸調用棧的深度就是n
那麼每次遞歸的空間複雜度是O(1), 調用棧深度爲n,
最後 這個遞歸算法的空間複雜度就是 n * 1 = O(n)
那麼剛剛這個遞歸求斐波那契數的算法 看上去簡潔 可時間複雜度是 O(2^n),可以說非常耗時
// 時間複雜度:O(2^n)
// 空間複雜度:O(n)
int fibonacci(int i) {
if(i <= 0) return 0;
if(i == 1) return 1;
return fibonacci(i-1) + fibonacci(i-2);
}
罪魁禍首就是這裏的兩次遞歸,導致了 時間複雜度以指數上升
其實這裏是可以優化的。 主要是減少遞歸的調用次數。
看如下優化後代碼:
// 時間複雜度:O(n)
// 空間複雜度:O(n)
int fibonacci(int first, int second, int n) {
if (n <= 0) {
return 0;
}
if (n < 3) {
return 1;
}
else if (n == 3) {
return first + second;
}
else {
return fibonacci(second, first + second, n - 1);
}
}
可以看出在遞歸的時候是線性的,時間複雜度是 O(n)
我們來總結一下 剛剛分析的幾種斐波那契數的算法
從這我們可以看出,求斐波那契數的時候,使用遞歸算法並不一定是在性能上是最優的,但遞歸確實可以讓代碼看上去很簡單。
最後帶大家在分析一段代碼,二分查找的遞歸實現
都知道二分查找的時間複雜度是logn,那麼遞歸二分查找的空間複雜度是多少呢,
// 時間複雜度:O(logn)
// 空間複雜度:O(nlogn)
int binary_search( int arr[], int l, int r, int x) {
if (r >= l) {
int mid = l + (r - l) / 2;
if (arr[mid] == x)
return mid;
if (arr[mid] > x)
return binary_search(arr, l, mid - 1, x);
return binary_search(arr, mid + 1, r, x);
}
return -1;
}
我們依然看 每次遞歸的空間複雜度 和 遞歸的深度
首先我們先明確空間複雜度裏面的n是什麼, 二分查找的時候n就是指查找數組的長度
每次遞歸的空間複雜度 可以看出主要就是參數裏傳入的這個數組arr,也就是 O(n)
那麼遞歸深度呢,二分查找的遞歸深度是logn ,遞歸深度就是調用棧的長度,以這段代碼的空間複雜度爲O(nlogn)
那麼有同學問了,爲什麼網上很多人說遞歸二分查找 的空間複雜度是O(logn),而不是O(nlogn)呢
其實很多文章都沒有說清楚,還是這個數組arr
如果我們把這個數組arr定義爲全局變量而不是放在遞歸裏面
那麼 每次遞歸的空間複雜度爲O(1) 和 遞歸的深度logn, 所以空間複雜度爲O(logn),
代碼如下:
// 時間複雜度:O(logn)
// 空間複雜度:O(logn)
int arr[] = {2, 3, 4, 5, 8, 10, 15, 17, 20};
int binary_search(int l, int r, int n) {
if (r >= l) {
int mid = l + (r - l) / 2;
if (arr[mid] == n)
return mid;
if (arr[mid] > n)
return binary_search(l, mid - 1, n);
return binary_search(mid + 1, r, n);
}
return -1;
}
希望通過這篇文章可以幫助大家對空間複雜度有進一步的認識 ,在算法面試的時候 才能更充分體現出自己對算法的理解和思考