第2課:算法複雜度分析(上):時間、空間複雜度分析法

1、算法的考量指標

算法的考量指標,我們是用時間、空間複雜度來衡量的。

時間複雜度的全稱是漸進時間複雜度,表示算法的執行時間與數據規模之間的增長關係。
空間複雜度全稱就是漸進空間複雜度,表示算法的存儲空間與數據規模之間的增長關係。

2、爲什麼需要複雜度分析?

我把代碼跑一遍,通過統計、監控,就能得到算法執行的時間和佔用的內存大小
這種評估算法執行效率的方法是正確的。很多數據結構和算法書籍還給這種方法起了一個名字,叫事後統計法。但是,這種統計方法有非常大的侷限性。
  • 1.測試結果非常依賴測試環境測試環境中硬件的不同會對測試結果有很大的影響
    比如,我們拿同樣一段代碼,分別用Intel Core i9處理器和 Intel Core i3 處理器來運行,不用說,i9 處理器要比 i3 處理器執行的速度快很多。還有,比如原本在這臺機器上 a 代碼執行的速度比 b 代碼要快,等我們換到另一臺機器上時,可能會有截然相反的結果。

  • 2.測試結果受數據規模的影響很大
    後面我們會講排序算法,我們先拿它舉個例子。對同一個排序算法,待排序數據的有序度不一樣,排序的執行時間就會有很大的差別。極端情況下,如果數據已經是有序的,那排序算法不需要做任何操作,執行時間就會非常短。除此之外,如果測試數據規模太小,測試結果可能無法真實地反應算法的性能。比如,對於小規模的數據排序,插入排序可能反倒會比快速排序要快!所以,我們需要一個不用具體的測試數據來測試,就可以粗略地估計算法的執行效率的方法。這就是我們今天要講的時間、空間複雜度分析方法。

3、大O表示法

大O表示法:算法的時間複雜度通常用大O符號表述,定義爲T[n] = O(f(n))。稱函數T(n)以f(n)爲界或者稱T(n)受限於f(n)。 如果一個問題的規模是n,解這一問題的某一算法所需要的時間爲T(n)。T(n)稱爲這一算法的“時間複雜度”。當輸入量n逐漸加大時,時間複雜度的極限情形稱爲算法的“漸近時間複雜度”。

#例1 int sum(int n) {
   int result = 0;
   int i = 1;
   for (; i <= n; ++i) {
     result = result + i;
   }
   return result;
 }

分析:假設每行代碼執行的時間都一樣,爲 unit_time。
第 2、3 行代碼分別需要 1 個 unit_time 的執行時間,第 4、5 行都運行了 n 遍,
所以需要 2n * unit_time 的執行時間,所以這段代碼總的執行時間就是 (2n+2) * unit_time。
可以看出來,所有代碼的執行時間 T(n) 與每行代碼的執行次數成正比。
再看看下面這個例子,同上面的分析方法,我們得出這段代碼總的執行時間就是 (2n^2+2n+3)*unit_time。

例2:int sum(int n) {
   int result = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1;
     for (; j <= n; ++j) {
       result = result +  i * j;
     }
   }
 }

把這個規律總結成一個公式:T(n) = O(f(n))

4、如何分析一段代碼的時間複雜度?

  • 只關注循環執行次數最多的一段代碼
    \color{red}{我們通常會忽略掉公式中的常量、低階、係數,只需要記錄一個最大階的量級就可以了。}
    例子1中:其中第 2、3、 行代碼都是常量級的執行時間,與 n 的大小無關,所以對於複雜度並沒有影響。
    4、5行代碼循環執行,與 n 的大小無關,次數最多的是第4、5行代碼,所以這塊代碼要重點分析。前面我們也講過,這兩行代碼被執行了 n 次,所以總的時間複雜度就是 O(n)。
    例子2中時間複雜度爲:O(n^2)

  • 加法法則:總複雜度等於量級最大的那段代碼的複雜度

例3:int sum(int n) {
   int result_1 = 0;
   int x = 1;
   for (; x < 1000; ++x) {
     result_1 = result_1 + x;
   }

   int result_2 = 0;
   int y = 1;
   for (; y < n; ++y) {
     result_2 = result_2 + y;
   }
 
   int result_3 = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1; 
     for (; j <= n; ++j) {
       result_3 = result_3 +  i * j;
     }
   }
   return result_1 + result_2 + result_3;
 }

這個例子分三部分:求result_1、result_2、result_3。
第一部分跟n沒關係:屬於常量階,我們表示爲0(1)
第二部分:O(n)
第三部分爲:O(n^2)
所以整個sum函數的時間複雜度爲:T(n)=O(1)+T1(n)+T2(n)=max(O(f(n)), O(g(n)))=max(O(n),O(n^2)) = n^2

  • 乘法法則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積
    如果 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)).
    也就是說,假設 T1(n) = O(n),T2(n) = O(n2),則 T1(n) * T2(n) = O(n3)
例4: int sum1(int n) {
   int result = 0;
   int i = 1;
   for (; i <= n; ++i) {
     result = sum(i) + i;
   }
   return result;
 }
 
int sum(int n) {
   int result = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1;
     for (; j <= n; ++j) {
       result = result +  i * j;
     }
   }
 }

5、常見時間複雜度分析

常見時間複雜度.png

  • O(m+n)、O(m*n)
    代碼的複雜度由兩個數據的規模來決定,如例5:m 和 n 是表示兩個數據規模。我們無法事先評估 m 和 n 誰的量級大,所以我們在表示複雜度的時候,就不能簡單地利用加法法則,省略掉其中一個。所以,上面代碼的時間複雜度就是 O(m+n)。針對這種情況,原來的加法法則就不正確了,我們需要將加法規則改爲:T1(m) + T2(n) = O(f(m) + g(n))。但是乘法法則繼續有效:T1(m)*T2(n) = O(f(m) * f(n))。
例5: int cal(int m, int n) {
  int sum_1 = 0;
  int i = 1;
  for (; i < m; ++i) {
    sum_1 = sum_1 + i;
  }

  int sum_2 = 0;
  int j = 1;
  for (; j < n; ++j) {
    sum_2 = sum_2 + j;
  }

  return sum_1 + sum_2;
}

6、空間複雜度

空間複雜度比較簡單,空間複雜度(Space Complexity)是對一個算法在運行過程中臨時佔用存儲空間大小的量度,記做S(n)=O(f(n))。看一個例子。

例6: int sum(int n) {
  int result = 0;
  int[] a = new int[n]; // 開闢了新的存儲空間
  for (i; i <n; ++i) {
    a[i] = i * i;
  }
  for (i = n-1; i >= 0; --i) {
    result +=a[i];
  }
  return result;
}

除了第三行申請了一個大小爲 n 的 int 類型數組,除此之外,剩下的代碼都沒有佔用更多的空間,所以整段代碼的空間複雜度就是 O(n)。
我們常見的空間複雜度就是 O(1)、O(n)、O(n2),像 O(logn)、O(nlogn) 這樣的對數階複雜度平時都用不到。而且,空間複雜度分析比時間複雜度分析要簡單很多。所以,對於空間複雜度,掌握剛我說的這些內容已經足夠了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章