如何分析、統計算法的執行效率和資源消耗?算法
時間、空間複雜度分析。數據結構
爲何須要複雜度分析?數據結構和算法
你可能會有些疑惑,我把代碼跑一遍,經過統計、監控,就能獲得算法執行的時間和佔用的內存大 小。爲何還要作時間、空間複雜度分析呢?這種分析方法能比我實實在在跑一遍獲得的數據更準 確嗎? 首先,我能夠確定地說,你這種評估算法執行效率的方法是正確的。不少數據結構和算法書籍還給 這種方法起了一個名字,叫過後統計法。可是,這種統計方法有很是大的侷限性。函數
1.測試結果很是依賴測試環境。測試
2.測試結果受數據規模影響很大spa
3.大O複雜度表示法code
1 int cal(int n) { 2 int sum = 0; 3 int 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) 與每行代碼的執行次數成正比。blog
按照這個分析思路,咱們再來看這段代碼。內存
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…