1. 程式人生 > >數據結構與算法之美02

數據結構與算法之美02

相加 關註 nom 規模 表達式 adding ont 總結 推導

二、算法復雜度分析

如何分析、統計算法的執行效率和資源消耗?

時間、空間復雜度分析。

為什麽需要復雜度分析?

你可能會有些疑惑,我把代碼跑一遍,通過統計、監控,就能得到算法執行的時間和占用的內存大 小。為什麽還要做時間、空間復雜度分析呢?這種分析方法能比我實實在在跑一遍得到的數據更準 確嗎? 首先,我可以肯定地說,你這種評估算法執行效率的方法是正確的。很多數據結構和算法書籍還給 這種方法起了一個名字,叫事後統計法。但是,這種統計方法有非常大的局限性。

1.測試結果非常依賴測試環境。

2.測試結果受數據規模影響很大

3.大O復雜度表示法

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) 與每行代碼的執行次數成正比。

按照這個分析思路,我們再來看這段代碼。

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 ) 。

時間復雜度分析

如何分析一段代碼的時間復雜度?有三個比較實用的方法。

  1. 只關註循環執行次數最多的一段代碼 大 O 這種復雜度表示方法只是表示一種變化趨勢。我們通常會忽略掉公式中的常量、低階、系數,只需要記錄一個最大階的量級就可以了。所以,我們在分析一個算法、一段代碼的時間復雜度的時候,也只關註循環執行次數最多的那一段代碼就可以了。這段核心代碼執行次數的 n的量級,就是整段要分析代碼的時間復雜度。

    那前面的第一個例子來說,其中第 2 、 3 行代碼都是常量級的執行時間,與 n 的大小無關,所以對於復雜度並沒有影響。循環執行次數最多的是第 4 、 5 行代碼,所以這塊代碼要重點分析。前面我們也講過,這兩行代碼被執行了 n 次,所以總的時間復雜度就是 O(n)

  1. 加法法則:總復雜度等於量級最大的那段代碼的復雜度

//前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