前端進階演算法1:如何分析、統計演算法的執行效率和資源消耗?

前端瓶子君發表於2020-03-31

簡介

前端還要學演算法?必須學,而且必須狠狠地學。現在去大廠面試,資料結構與演算法已經是標配,要是不會的話,那基本與大廠無緣了。

作為一名前端,雖然在平常開發中很少寫演算法,但當我們需要深入前端框架、開發語言、開源庫時,懂演算法將大大提高我們看原始碼的能力。例如 react 的 diff 演算法、webpack 中利用 tree-shaking 優化、v8 中的呼叫棧、訊息佇列等,這些就大量使用了演算法,看懂了就能更好的瞭解它們的效能,更高效的解決問題,進階到更高 Level,賺更多錢。

現在市面上的演算法資料很多,但針對前端的演算法資料少之又少,所以,這裡我整理了一份適用於前端的資料結構與演算法系列,希望能幫助你從0到1構建完整的資料結構與演算法體系。

本系列預估一共有40多篇,本篇是第一篇。想要更多更快的學習本系列,可以關注公眾號「前端瓶子君」和我的「Github(點選檢視)

一、為什麼引入複雜度分析

我們知道,好的資料結構與演算法能夠大大縮短程式碼的執行時間與儲存空間,那麼我們如何去衡量它喃?本節就主要介紹演算法效能的衡量指標—複雜度分析。

判斷一段程式碼執行的效率最簡單最直觀的方式就是把它放在機器上執行一遍,自然就會得到演算法的執行時間與佔用記憶體大小。那麼為什麼還要引入複雜度分析喃?

這主要是因為,通過機器上執行程式碼來統計演算法的效能,有很大的侷限性,它容易受測試環境、資料規模影響:

  • 統計結果易受測試環境影響:不同系統、處理器的機器測試結果可能出現很大的不同
  • 統計結果易受資料本身、資料規模影響:不同的資料、不同長度的資料都可能對測試結果產生巨大的影響

而這些都不是我們想要看到的。我們需要的是不受外在因素影響的、大致的估計演算法執行效率的方法。這就是使用複雜度分析的原因。

二、如何表示複雜度

如何表示演算法複雜度,具體來講就是程式碼執行的時間、執行消耗的儲存空間。例如:

function cal(n) {
    let sum = 0; // 1 unit_time
    let i = 0; // 1 unit_time
    for(; i <= n; i++) { // n unit_time
        sum += i; // n unit_time
    }
    return sum
}
複製程式碼

從 CPU 的角度看,每段程式碼不過是讀寫資料或運算元據,儘管每次操作 CPU 執行的個數、執行的時間都不同,但我們粗咯把每次執行的時間都一致,稱為 unit_time

所以上述程式碼總共執行 (2n+2)*unit_time 時間,即:T(n)=(2n+2)*unit_time ,所以,我們可以寫成:

T(n) = O(f(n))
複製程式碼

其中:

  • n:表示資料規模的大小
  • f(n):表示每行程式碼執行的次數總和
  • O:表示程式碼的執行時間 T(n) 與 f(n) 表示式成正比

當 n 很大時,例如 10000,甚至更大,T(n) = O(f(n)) 可以表示為 T(n) = O(n)

這就是大 O 時間複雜度表示法大 O 時間複雜度實際上並不具體表示程式碼真正的執行時間,而是表示程式碼執行時間隨資料規模增長的變化趨勢,所以,也叫作漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度

三、時間複雜度

當 n 無限大時,時間複雜度 T(n) 受 n 的最高數量級影響最大,與f(n) 中的常量、低階、係數關係就不那麼大了。所以我們分析程式碼的時間複雜度時,僅僅關注程式碼執行次數最多的那段就可以了。

看一個例子:

function fun1(n) {
    let sum = 0,i = 0; 
    for(; i <= n; i++) {
        sum += i; 
    }
    return sum
}
function fun2(n) {
    let sum = 0, sum1 = 0, i = 0, j = 0; 
    for(; i <= n; i++) { // 迴圈1
        sum += i; 
    }
    for(i = 0; i <= n; i++) { // 迴圈2
        for(j = 0; j <= n; j++) { 
            sum += i * j; 
        }
    }
    return sum
}
function fun3(n) {
    let sum = 0, i = 0; 
    for(; i <= n; i++) { 
        sum += fun(i); 
    }
    return sum
}
複製程式碼

fun1 中第1行都是常量,對 n 的影響不大,所以總的時間複雜度要看第2、3行的迴圈,即時間複雜度為: O(n)

fun2 中迴圈1的時間複雜度為 O(n) ,迴圈2的時間複雜度為 O(n2),當 n 趨於無窮大時,總體的時間複雜度要趨於 O(n2) ,即上面程式碼的時間複雜度是 O(n2)。

fun3 的時間複雜度是 O(n * T(fun)) = O(n*n) ,即 O(n2) 。

所以:T(c+n)=O(n),T(m+n)=O(max(m, n)),T(n) = T1(n) T2(m) = O(nm),其中 c 為常量

常見覆雜度(按數量階遞增)

多項式量級:

  • 常量階: O(1):當演算法中不存在迴圈語句、遞迴語句,即使有成千上萬行的程式碼,其時間複雜度也是Ο(1)
  • 對數階:O(logn): 簡單介紹一下
let i=1;
while (i <= n)  {
  i = i * 2;
}
複製程式碼
  • 每次迴圈 i 都乘以 2 ,直至 i > n ,即執行過程是:20、21、22、…、2k、…、2x、 n 所以總執行次數 x ,可以寫成 2x = n ,則時間複雜度為 O(log2n) 。這裡是 2 ,也可以是其他常量 k ,時間複雜度也是: O(log3n) = O(log32 * log2n) = O(log2n)
  • 線性階:O(n)
  • 線性對數階:O(nlogn)
  • 平方階、立方階、….、k次方階:O(n2)、O(n3)、…、O(nk)

非多項式量階:

  • 指數階:O(2n)
  • 階乘階:O(n!)

四、空間複雜度

時間複雜度表示演算法的執行時間與資料規模之間的增長關係。類比一下,空間複雜度表示演算法的儲存空間與資料規模之間的增長關係。例如:

function fun(n) {
    let a = [];
    for (let i = 0; i < n; i++) {
        a.push(i);
    }
    return a;
}
複製程式碼

以上程式碼我們可以清晰的看出程式碼執行的空間為 O(1+n) = O(n),即為 i 及陣列 a 佔用的儲存空間。

所以,空間複雜度分析比時間複雜度分析要簡單很多。

五、平均時間複雜度

時間複雜度受資料本身影響,還分為:

  • 最好時間複雜度:在最理想的情況下,執行這段程式碼的時間複雜度
  • 最壞時間複雜度:在最糟糕的情況下,執行這段程式碼的時間複雜度
  • 平均時間複雜度:所有情況下,求一個平均值,可以省略掉係數、低階、常量

六、參考資料

極客時間的資料結構與演算法之美

學習JavaScript資料結構與演算法

七、認識更多的前端道友,一起進階前端開發

前端演算法集訓營第一期免費開營啦???,免費喲!

在這裡,你可以和志同道合的前端朋友們一起進階前端演算法,從0到1構建完整的資料結構與演算法體系。

掃碼加入【前端演算法交流群交流群】,若二維碼失效後,可在公眾號「前端瓶子君」內回覆「演算法」

前端進階演算法1:如何分析、統計演算法的執行效率和資源消耗?

最後推薦一款前端刷題神器:》》面試官都在用的題庫,掃碼學習《《

img

相關文章