復雜度分析是什么?
復雜度分析就是分析執行一個給定算法需要消耗的計算資源數量(例如計算時間,存儲器使用等)的過程。
為什么要學習復雜度分析?
沒有復雜度分析怎么得到算法執行的時間和占用的內存大小
把代碼運行一遍,通過統計、監控,就能得到算法執行的時間和占用的內存大小。
該方法的缺點在於:
1、測試結果非常依賴測試環境
拿同樣一段代碼,在 Intel Core i9 處理器上運行的速度肯定要比 Intel Core i3 快得多。同一段代碼,在不同機器上運行,也可能會有截然相反的結果。
2、測試結果受數據規模的影響很大
以排序算法舉個例子。對同一個排序算法,待排序數據的有序度不一樣,排序的執行時間就會有很大的差別。極端情況下,如果數據已經是有序的,那排序算法不需要做任何操作,執行時間就會非常短。除此之外,如果測試數據規模太小,測試結果可能無法真實地反應算法的性能。比如,對於小規模的數據排序,插入排序可能反倒會比快速排序要快!
使用復雜度分析有什么好處
不需要用具體的測試數據來測試,就可以粗略地估計算法的執行效率。
怎么學習復雜度分析?
大O復雜度表示法
下面有一段代碼,來估算一下這段代碼的執行時間:
1 int cal(int n) { 2 int sum = 0; 3 int i = 0; 4 int j = 0; 5 for (; i < n; ++i) { 6 j = 1; 7 for (; j < n; ++j) { 8 sum = sum + i * j; 9 } 10 } 11 }
假設每行代碼執行的時間都一樣,為 unit_time,在這個假設的基礎之上,我們可以看出:第 2、3、4 行代碼分別需要 1 個 unit_time 的執行時間,第 5、6 行都運行了 n 遍,所以需要 2n*unit_time 的執行時間,第 7、8 行代碼循環執行了 n^2遍,所以需要 2n^2 * unit_time 的執行時間。所以,整段代碼總的執行時間 T(n) = (2n^2+2n+3)*unit_time。
通過這段代碼執行時間的推導過程,我們可以知道,所有代碼的執行時間 T(n) 與每行代碼的執行次數 n 成正比。用大 O 復雜度表示法可以這樣表示:T(n) = O(f(n))
其中,T(n) 表示代碼執行的時間;n 表示數據規模的大小;f(n) 表示每行代碼執行的次數總和。公式中的 O,表示代碼的執行時間 T(n) 與 f(n) 表達式成正比。
時間復雜度分析
分析一段代碼的時間復雜度三個比較實用的方法:
1、只關注循環執行次數最多的一段代碼
在進行時間復雜度分析的時候,我們通常會忽略掉公式中的常量、低階、系數,只記錄一個最大階的量級。所以,我們在分析一個算法、一段代碼的時間復雜度的時候,也只關注循環執行次數最多的那一段代碼就可以了。
2、加法法則:總復雜度等於量級最大的那段代碼的復雜度
假設有這樣三段代碼,每段代碼是一個for循環,第一段代碼循環了 100 次,第二段代碼循環了 n 次,第三段代碼循環了 n^2 次。
那么,第一段代碼的時間復雜度是一個常量,跟 n 的規模無關。第二段代碼和第三段代碼的時間復雜度分別為 O(n) 和 O(n^2)。
因為總的時間復雜度就等於量級最大的那段代碼的時間復雜度,所以,整段代碼的時間復雜度為 O(n^2)。那我們將這個規律抽象成公式就是:
如果 T1(n)=O(f(n)),T2(n)=O(g(n));
那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n)))
3、乘法法則:嵌套代碼的復雜度等於嵌套內外代碼復雜度的乘積
復雜度分析中還有一個乘法法則。
如果 T1(n)=O(f(n)),T2(n)=O(g(n));
那么 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n))。
常見的嵌套循環,其時間復雜度就可以用乘法法則進行計算 。
幾種常見時間復雜度實例分析
常見的復雜度量級並不多,大概就是下圖中包含的:
復雜度量級可以分為多項式量級和非多項式量級。其中,非多項式量級只有兩個:O(2^n) 和 O(n!)。
對於多項式量級,隨着數據規模的增長,算法的執行時間和空間占用統一呈多項式規律增長;而對於非多項式量級,隨着數據規模的增長,其時間復雜度會急劇增長,執行時間無限增加;
這里主要來看幾種常見的多項式時間復雜度:
1、O(1)
O(1) 只是常量級時間復雜度的一種表示方法,並不是指只執行了一行代碼。只要代碼的執行時間不隨 n 的增大而增長,這樣代碼的時間復雜度我們都記作 O(1)。或者說,一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬行的代碼,其時間復雜度也是Ο(1)。
2、O(logn)、O(nlogn)
最難分析的是對數階時間復雜度,這里通過一個例子來說明一下。
1 i = 1; 2 while (i <= n) { 3 i = i * 2; 4 }
我們已經知道,只要關注循環執行次數最多的那一段代碼就可以了,這里循環執行次數最多的是第 3 行代碼,所以,我們只要能計算出這行代碼被執行了多少次,就能知道整段代碼的時間復雜度。 從代碼中可以看出,變量 i 的值從 1 開始取,每循環一次就乘以 2。當大於 n 時,循環結束。觀察變量 i 的增長規律,我們可以發現,它的取值是一個等比數列。如果我們把它一個一個列出來,就應該是這個樣子的:
理解了 O(logn),那 O(nlogn) 就很容易理解了。前面我們講了時間復雜度分析的乘法法則,如果一段代碼的時間復雜度是 O(logn),我們循環執行 n 遍,那么時間復雜度就是 O(nlogn) 了。(常見的歸並排序、快速排序的時間復雜度都是 O(nlogn))
3、O(m+n)、O(m*n)
來看這樣一段代碼:
1 int cal(int m, int n) { 2 int sum_1 = 0; 3 int i = 0; 4 for (; i < m; ++i) { 5 sum_1 = sum_1 + i; 6 } 7 8 int sum_2 = 0; 9 int j = 0; 10 for (; j < n; ++j) { 11 sum_2 = sum_2 + j; 12 } 13 14 return sum_1 + sum_2; 15 }
從代碼中可以看出,m 和 n 是表示兩個未知的數據規模,其大小關系也無法確定,所以我們在表示復雜度的時候,就不能簡單地利用加法法則,省略掉其中一個。所以,上面代碼的時間復雜度就是 O(m+n)。
針對這種情況,原來的加法法則就不正確了,我們需要將加法規則改為:T1(m) + T2(n) = O(f(m) + g(n))。但是乘法法則繼續有效:T1(m)*T2(n) = O(f(m) * f(n))。
空間復雜度分析
相比起時間復雜度分析,空間復雜度分析方法學起來就非常簡單了。
這里也舉一個例子進行說明:
1 void print(int n) {
2 int i = 0; 3 int[] a = new int[n]; 4 for (i; i <n; ++i) { 5 a[i] = i * i; 6 } 7 }
這里第 3 行申請了一個大小為 n 的 int 類型數組,除此之外,其余代碼所占空間都是可以忽略的,所以整段代碼的空間復雜度就是 O(n)。
我們常見的空間復雜度就是 O(1)、O(n)、O(n^2 ),像 O(logn)、O(nlogn) 這樣的對數階空間復雜度平時都用不到,相比起來,空間復雜度分析比時間復雜度分析要簡單很多。
內容小結
總結一下:復雜度包括時間復雜度和空間復雜度,用來分析算法執行效率與數據規模之間的增長關系,越高階復雜度的算法,執行效率越低。常見的復雜度從低階到高階有:O(1)、O(logn)、O(n)、O(nlogn)、O(n^2 )。