數據結構與算法之美02
二、算法復雜度分析
如何分析、統計算法的執行效率和資源消耗?
時間、空間復雜度分析。
為什麽需要復雜度分析?
你可能會有些疑惑,我把代碼跑一遍,通過統計、監控,就能得到算法執行的時間和占用的內存大 小。為什麽還要做時間、空間復雜度分析呢?這種分析方法能比我實實在在跑一遍得到的數據更準 確嗎? 首先,我可以肯定地說,你這種評估算法執行效率的方法是正確的。很多數據結構和算法書籍還給 這種方法起了一個名字,叫事後統計法。但是,這種統計方法有非常大的局限性。
1.測試結果非常依賴測試環境。
2.測試結果受數據規模影響很大
3.大O復雜度表示法
1 int cal(int n) { 2 int sum = 0; 3int i = 1; 4 for (; i <= n; ++i) { 5 sum = sum + i; 6 } 7 return sum; 8 }
從 CPU 的角度來看,這段代碼的每一行都執行著類似的操作:讀數據 - 運算 - 寫數據。盡管每行代碼 對應的 CPU 執行的個數、執行的時間都不一樣,但是,我們這裏只是粗略估計,所以可以假設每 行代碼執行的時間都一樣,為 unit_time 。在這個假設的基礎之上,這段代碼的總執行時間是多少 呢? 第 2 、 3 行代碼分別需要 1 個 unit_time 的執行時間,第 4 、 5 行都運行了 n 遍,所以需要 2nunit_time 的執行時間,所以這段代碼總的執行時間就是 (2n+2)*unit_time 。可以看出來,所有代 碼的執行時間 T(n) 與每行代碼的執行次數成正比。
按照這個分析思路,我們再來看這段代碼。
1 int cal(int n) { 2 int sum = 0; 3 int i = 1; 4 int j = 1; 5 for (; i <= n; ++i) { 6 j = 1; 7 for (; j <= n; ++j) { 8 sum = sum + i * j; 9 } 10 } 11 }
我們依舊假設每個語句的執行時間是 unit_time 。那這段代碼的總執行時間 T(n) 是多少呢? 第 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.
盡管我們不知道 unit_time 的具體值,但是通過這兩段代碼執行時間的推導過程,我們可以得到一 個非常重要的規律,那就是,所有代碼的執行時間 T(n) 與每行代碼的執行次數 n 成正比。 我們可以把這個規律總結成一個公式。
大O記號
其中, T(n)表示代碼執行的時間; n 表示數據規模的大小; f(n) 表示每行代碼執行的次數總和。因為這是一個公式,所以用 f(n) 來表示。公式中的O ,表示代碼的執行時間 T(n) 與 f(n) 表達式成正比。 所以,第一個例子中的 T(n) = O(2n+2) ,第二個例子中的 T(n) = O(2n +2n+3) 。
這就是大 O 時間復雜度表示法。
大 O 時間復雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間復雜度( asymptotic time complexity ),簡稱時間復雜度。 當 n 很大時,你可以把它想象成 10000 、 100000 。而公式中的低階、常量、系數三部分並不左右增 長趨勢,所以都可以忽略。我們只需要記錄一個最大量級就可以了,如果用大 O 表示法表示剛講的 那兩段代碼的時間復雜度,就可以記為: T(n) = O(n) ; T(n) = O(n ) 。
時間復雜度分析
如何分析一段代碼的時間復雜度?有三個比較實用的方法。
只關註循環執行次數最多的一段代碼 大 O 這種復雜度表示方法只是表示一種變化趨勢。我們通常會忽略掉公式中的常量、低階、系數,只需要記錄一個最大階的量級就可以了。所以,我們在分析一個算法、一段代碼的時間復雜度的時候,也只關註循環執行次數最多的那一段代碼就可以了。這段核心代碼執行次數的 n的量級,就是整段要分析代碼的時間復雜度。
那前面的第一個例子來說,其中第 2 、 3 行代碼都是常量級的執行時間,與 n 的大小無關,所以對於復雜度並沒有影響。循環執行次數最多的是第 4 、 5 行代碼,所以這塊代碼要重點分析。前面我們也講過,這兩行代碼被執行了 n 次,所以總的時間復雜度就是 O(n)
加法法則:總復雜度等於量級最大的那段代碼的復雜度
//前100個數相加 int cal(int n) { int sum_1 = 0; int p = 1; for (; p < 100; ++p) { sum_1 = sum_1 + p; } //前n個數 int sum_2 = 0; int q = 1; for (; q < n; ++q) { sum_2 = sum_2 + q; } // int sum_3 = 0; int i = 1; int j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum_3 = sum_3 + i * j; } } return sum_1 + sum_2 + sum_3; }
這個代碼分為三部分,分別是求 sum_1 、 sum_2 、 sum_3 。我們可以分別分析每一部分的時間復雜 度,然後把它們放到一塊兒,再取一個量級最大的作為整段代碼的復雜度。
第一段的時間復雜度是多少呢?這段代碼循環執行了 100 次,所以是一個常量的執行時間,跟 n 的 規模無關。 這裏我要再強調一下,即便這段代碼循環 10000 次、 100000 次,只要是一個已知的數,跟 n 無 關,照樣也是常量級的執行時間。當 n 無限大的時候,就可以忽略。盡管對代碼的執行時間會有很 大影響,但是回到時間復雜度的概念來說,它表示的是一個算法執行效率與數據規模增長的變化趨 勢,所以不管常量的執行時間多大,我們都可以忽略掉。因為它本身對增長趨勢並沒有影響。 那第二段代碼和第三段代碼的時間復雜度是多少呢?答案是 O(n) 和 O(n )。
綜合三段代碼的時間復雜度,我們取最大的量級,即總的復雜度為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)xT2(n)=O(f(n))xO(g(n))=O(f(n)xg(n)). 也就是說,假設T1(n)=O(n),T2(n)=O(n^2),則T1(n)xT2(n)=O(n^3)。
落實到具體的代碼上
1 int cal(int n) { 2 int ret = 0; 3 int i = 1; 4 for (; i < n; ++i) { 5 ret = ret + f(i); 6 } 7 } 8 9 int f(int n) { 10 int sum = 0; 11 int i = 1; 12 for (; i < n; ++i) { 13 sum = sum + i; 14 } 15 return sum; 16 }
我們單獨看cal()函數。假設f()只是一個普通的操作,那第4~6行的時間復雜度就是,T1(n)=O(n)。
但f()函數本身不是一個簡單的操作,它的時間復雜度是T2(n)=O(n),所以,整個cal)函數的時間復雜度就是,T(n)=T1(n)xT2(n)=O(nxn)=O(n2)。
常見的時間復雜度實例分析
分類:
多項式量級:
非多項式量級:O(2n)和O(n!)。
我們把復雜度為非多項式量級的算法問題叫做NP(Non-Deterministic Polynomial,非確定多項式)問題。
當數據規模n越來越大時,非多項式量級算法的執行時間會急劇增加,求解問題的執行時間會無限增長。所以,非多項式時間復雜度的算法其實是非常低效的算法。因此,關於NP時間復雜度問題略。
主要來看幾種常見的多項式時間復雜度。
Next…
數據結構與算法之美02