上篇文章講述了與複雜度有關的大 O 表示法和常見的時間複雜度量級,這篇文章來說講另外幾種複雜度: 遞歸算法的時間複雜度(recursive algorithm time complexity),最好狀況時間複雜度(best case time complexity)、最壞狀況時間複雜度(worst case time complexity)、平均時間複雜度(average case time complexity)和均攤時間複雜度(amortized time complexity)。算法
若是遞歸函數中,只進行一次遞歸調用,遞歸深度爲depth;數組
在每一個遞歸的函數中,時間複雜度爲T;bash
則整體的時間複雜度爲O(T * depth)。函數
在前面的學習中,歸併排序 與 快速排序 都帶有遞歸的思想,而且時間複雜度都是O(nlogn) ,但並非有遞歸的函數就必定是 O(nlogn) 級別的。從如下兩種狀況進行分析。學習
int binarySearch(int arr[], int l, int r, int target){
if( l > r ) return -1;
int mid = l + (r-l)/2;
if( arr[mid] == target ) return mid;
else if( arr[mid] > target )
return binarySearch(arr, l, mid-1, target); // 左邊
else
return binarySearch(arr, mid+1, r, target); // 右邊
}
複製代碼
好比在這段二分查找法的代碼中,每次在 [ l , r ] 範圍中去查找目標的位置,若是中間的元素 arr[mid]
不是 target
,那麼判斷 arr[mid]
是比 target
大 仍是 小 ,進而再次調用 binarySearch
這個函數。優化
在這個遞歸函數中,每一次沒有找到target
時,要麼調用 左邊 的 binarySearch
函數,要麼調用 右邊 的 binarySearch
函數。也就是說在這次遞歸中,最多調用了一次遞歸調用而已。根據數學知識,須要log2n次才能遞歸到底。所以,二分查找法的時間複雜度爲 O(logn)。ui
int sum (int n) {
if (n == 0) return 0;
return n + sum( n - 1 )
}
複製代碼
在這段代碼中比較容易理解遞歸深度隨輸入 n 的增長而線性遞增,所以時間複雜度爲 O (n)。spa
//遞歸深度:logn
//時間複雜度:O(logn)
double pow( double x, int n){
if (n == 0) return 1.0;
double t = pow(x,n/2);
if (n %2) return x*t*t;
return t * t;
}
複製代碼
遞歸深度爲 logn
,由於是求須要除以 2 多少次才能到底。code
遞歸算法中比較難計算的是屢次遞歸調用。cdn
先看下面這段代碼,有兩次遞歸調用。
// O(2^n) 指數級別的數量級,後續動態規劃的優化點
int f(int n){
if (n == 0) return 1;
return f(n-1) + f(n - 1);
}
複製代碼
遞歸樹中節點數就是代碼計算的調用次數。
好比 當 n = 3
時,調用次數計算公式爲
1 + 2 + 4 + 8 = 15
通常的,調用次數計算公式爲
2^0 + 2^1 + 2^2 + ...... + 2^n = 2^(n+1) - 1 = O(2^n)
與之有所相似的是 歸併排序 的遞歸樹,區別點在於
n
,而 歸併排序 的遞歸樹深度爲logn
。所以,在如 歸併排序 等排序算法中,每一層處理的數據量爲 O(n) 級別,同時有 logn
層,時間複雜度即是 O(nlogn)。
動圖代表的是在數組 array 中尋找變量 x 第一次出現的位置,若沒有找到,則返回 -1;不然返回位置下標。
int find(int[] array, int n, int x) {
for ( int i = 0 ; i < n; i++) {
if (array[i] == x) {
return i;
break;
}
}
return -1;
}
複製代碼
在這裏當數組中第一個元素就是要找的 x 時,時間複雜度是 O(1);而當最後一個元素纔是 x 時,時間複雜度則是 O(n)。
最好狀況時間複雜度就是在最理想狀況下執行代碼的時間複雜度,它的時間是最短的;最壞狀況時間複雜度就是在最糟糕狀況下執行代碼的時間複雜度,它的時間是最長的。
最好、最壞時間複雜度反應的是極端條件下的複雜度,發生的機率不大,不能表明平均水平。那麼爲了更好的表示平均狀況下的算法複雜度,就須要引入平均時間複雜度。
平均狀況時間複雜度可用代碼在全部可能狀況下執行次數的加權平均值表示。
仍是以 find
函數爲例,從機率的角度看, x 在數組中每個位置的可能性是相同的,爲 1 / n。那麼,那麼平均狀況時間複雜度就能夠用下面的方式計算:
((1 + 2 + ... + n) / n + n) / 2 = (3n + 1) / 4
find
函數的平均時間複雜度爲 O(n)。
咱們經過一個動態數組的 push_back
操做來理解 均攤複雜度。
template <typename T>
class MyVector{
private:
T* data;
int size; // 存儲數組中的元素個數
int capacity; // 存儲數組中能夠容納的最大的元素個數
// 複雜度爲 O(n)
void resize(int newCapacity){
T *newData = new T[newCapacity];
for( int i = 0 ; i < size ; i ++ ){
newData[i] = data[i];
}
data = newData;
capacity = newCapacity;
}
public:
MyVector(){
data = new T[100];
size = 0;
capacity = 100;
}
// 平均複雜度爲 O(1)
void push_back(T e){
if(size == capacity)
resize(2 * capacity);
data[size++] = e;
}
// 平均複雜度爲 O(1)
T pop_back(){
size --;
return data[size];
}
};
複製代碼
push_back
實現的功能是往數組的末尾增長一個元素,若是數組沒有滿,直接日後面插入元素;若是數組滿了,即 size == capacity
,則將數組擴容一倍,而後再插入元素。
例如,數組長度爲 n,則前 n 次調用 push_back
複雜度都爲 O(1) 級別;在第 n + 1 次則須要先進行 n 次元素轉移操做,而後再進行 1 次插入操做,複雜度爲 O(n)。
所以,平均來看:對於容量爲 n 的動態數組,前面添加元素須要消耗了 1 * n 的時間,擴容操做消耗 n 時間 , 總共就是 2 * n 的時間,所以均攤時間複雜度爲 O(2n / n) = O(2),也就是 O(1) 級別了。
能夠得出一個比較有意思的結論:一個相對比較耗時的操做,若是能保證它不會每次都被觸發,那麼這個相對比較耗時的操做,它所相應的時間是能夠分攤到其它的操做中來的。