資料結構與演算法學習-複雜度分析

by在水一方發表於2019-03-03

前言

這一篇筆記主要記錄總結了什麼是演算法複雜度?、為什要做演算法複雜度分析?、如何做演算法複雜度分析?、常用的複雜度級別?以及如何掌握複雜度分析?等問題。

演算法複雜度分析是什麼?

資料結構與演算法解決的是如何更省、更快的儲存和處理資料的問題。因此就需要一個考量效率和資源消耗的方法,這就是複雜度分析方法。

複雜度也叫漸進複雜度,其中包含時間複雜度空間複雜度兩個維度,是用來分析演算法執行時間(或者佔用空間)資料規模的增長關係,可以粗略的表示,越高階複雜度的演算法,執行效率越低。

為什麼要做演算法複雜度分析?

  1. 統計、監控等事後統計法的侷限性:

    1. 測試結果高度依賴測試環境
    2. 測試結果受資料規模影響很大
  2. 因此需要一個不用具體的測試資料就可以粗略地估計演算法的執行效率的方法。複雜度分析不依賴執行環境、成本低、可操作性強。

  3. 掌握複雜度分析,將能寫出效能更優的程式碼。

如何做演算法複雜度分析?

大 O 複雜度表示法

資料結構與演算法學習-複雜度分析

  • 所有程式碼的執行時間 T(n) 與每行程式碼的執行次數 n 成正比。
  • 大 O 時間複雜度實際上並不具體代表程式碼真正的執行時間,而是表示程式碼執行時間隨資料規模增長變化趨勢,叫做漸進時間複雜度,簡稱時間複雜度
  • 由於公式中的低階、常量、係數對增長趨勢不影響,所以都可以忽略,只需要記錄一個最大的量級。

演算法複雜度分析法則

  1. 只關注迴圈執行次數最多的一段程式碼
  2. 加法法則:總複雜度等於量級最大的那段程式碼的複雜度
  3. 乘法法則:巢狀程式碼的複雜度等於巢狀內外程式碼複雜度的乘積

空間複雜度分析

類比時間複雜度,空間複雜度全稱就是漸進空間複雜度,表示演算法的儲存空間資料規模之間增長關係

常見的空間複雜度是:O(1)、O(n)、O(n^2)

最好、最壞時間複雜度

對有些程式碼,其複雜度有不是固定的,有最好、最壞的情況。如陣列元素查詢,有可能第一次就找到,這個時候演算法時間複雜度是常數階O(1),也有可能需要整個遍歷一遍陣列才能找到,這個時候是最壞的時間複雜度O(n)。

  • 最好時間複雜度:在最理想情況下,程式碼執行的時間複雜度。
  • 最壞時間複雜度:在最糟糕的情況下,程式碼執行的時間複雜度。

平均情況複雜度

由於最好和最壞情況複雜度對應的都是極端情況下的程式碼複雜度,發生的概率並不大,所以需要引入平均情況複雜度來更好的表示演算法的時間複雜度。

平均情況複雜度 是將各種情況下時間複雜度發生的概率考慮進去,然後計算整體時間複雜度的期望值,所以平均情況複雜度也叫做加權平均時間複雜度或者期望時間複雜度

很多時候,只要使用一個複雜度就可以滿足需求了。只有同一塊程式碼在不同情況下,時間複雜度有量級的差距,才會使用這三種複雜度表示法來區分。

均攤時間複雜度

**適用場景:**對一個資料結構進行一組連續操作中,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度比較高,而且這些操作之間存在前後連貫的時序關係,這個情況下,就可以將這一組操作放在一起分析,看能否將較高時間複雜度的那次操作平攤到其他那些時間複雜度較低的操作上。一般在能夠應用均攤時間複雜度的場景,均攤時間複雜度就等於最好情況時間複雜度

分析方法:攤還分析

常用的演算法複雜度級別

資料結構與演算法學習-複雜度分析

  • 非多項式量級

    • O(2^n) 和 O(n!)
    • 當資料規模 n 越大時,非多項式量級的演算法的執行時間會急劇增加。所以非多項式量級演算法是非常低效的演算法,因此要避免使用到。
  • 多項式量級

    • O(1):程式碼的執行時間不隨 n 的增大而增長,這樣程式碼的時間複雜度記做 O(1),一般情況下,如果演算法中不存在迴圈語句、遞迴語句、不管有多少行程式碼,其時間複雜度都是 O(1)。
    • O(logn)、O(nlogn):對數階複雜度。
    • O(m+n)、O(m*n):程式碼的複雜度由兩個資料的規模來決定。
  • 多項式量級演算法時間複雜度隨資料規模的增長變化趨勢曲線

資料結構與演算法學習-複雜度分析

如何掌握複雜度分析

多練習。

課後問題

1. 專案之前都會進行效能測試,再做程式碼的時間複雜度、空間複雜度分析是不是多次一舉?

**解答:**不是多此一舉,漸進時間,空間複雜度分析為我們提供了一個很好的理論分析的方向,成本低,可操作性強,不用具體的測試資料就可以粗略地估計演算法的執行效率,有助於寫出效能高效的程式碼,同時培養起演算法複雜度分析的思維,可以和效能測試相輔相成。

2. 分析以下程式碼的複雜度

// 全域性變數,大小為 10 的陣列 array,長度 len,下標 i。
int array[] = new int[10]; 
int len = 10;
int i = 0;

// 往陣列中新增一個元素
void add(int element) {
   if (i >= len) { // 陣列空間不夠了
     // 重新申請一個 2 倍大小的陣列空間
     int new_array[] = new int[len*2];
     // 把原來 array 陣列中的資料依次 copy 到 new_array
     for (int j = 0; j < len; ++j) {
       new_array[j] = array[j];
     }
     // new_array 複製給 array,array 現在大小就是 2 倍 len 了
     array = new_array;
     len = 2 * len;
   }
   // 將 element 放到下標為 i 的位置,下標 i 加一
   array[i] = element;
   ++i;
}
複製程式碼

解答:

  • 最好時間複雜度:當 i 小於 len 時,程式碼不執行 if 語句,演算法時間複雜度是 O(1),
  • 最壞時間複雜度:當 i >= len 時,程式碼開始執行 if 語句,裡面有個迴圈操作,所以演算法時間複雜度是 O(n)。
  • 平均情況複雜度:進入 if 語句後,陣列擴容 2 倍,陣列每次插入元素的概率都是 1/len + 1,所以根據加權時間複雜度計算公式為 1 * 1/len + 1 + 1 * 1/len + 1 + ... + len *(1/(len+1)) = 1,所以平均情況複雜度是 O(1)。
  • 均攤時間複雜度:每次 O(len)前都有 len 次 O(1),存在前後連貫的時序關係,所以可以將高時間複雜度的 O(len) 平攤到之前的 len 次上,所以均攤時間複雜度是 O(1)。

分享個人技術學習記錄和跑步馬拉松訓練比賽、讀書筆記等內容,感興趣的朋友可以關注我的公眾號「青爭哥哥」。

資料結構與演算法學習-複雜度分析

相關文章