復雜度分析(上):如何分析、統計算法的執行效率和資源消耗


復雜度分析是什么?

復雜度分析就是分析執行一個給定算法需要消耗的計算資源數量(例如計算時間,存儲器使用等)的過程。

為什么要學習復雜度分析?

沒有復雜度分析怎么得到算法執行的時間和占用的內存大小

把代碼運行一遍,通過統計、監控,就能得到算法執行的時間和占用的內存大小。

該方法的缺點在於:

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 )。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM