看動畫輕鬆理解時間複雜度(一)

程式設計師吳師兄發表於2018-12-13

原文連結:看動畫輕鬆理解時間複雜度(一)

演算法(Algorithm)是指用來運算元據、解決程式問題的一組方法。對於同一個問題,使用不同的演算法,也許最終得到的結果是一樣的,比如排序就有前面的十大經典排序和幾種奇葩排序,雖然結果相同,但在過程中消耗的資源和時間卻會有很大的區別,比如快速排序與猴子排序:)。

那麼我們應該如何去衡量不同演算法之間的優劣呢?

主要還是從演算法所佔用的「時間」和「空間」兩個維度去考量。

  • 時間維度:是指執行當前演算法所消耗的時間,我們通常用「時間複雜度」來描述。

  • 空間維度:是指執行當前演算法需要佔用多少記憶體空間,我們通常用「空間複雜度」來描述。

本小節將從「時間」的維度進行分析。

什麼是大O

當看「時間」二字,我們肯定可以想到將該演算法程式執行一篇,通過執行的時間很容易就知道複雜度了。

這種方式可以嗎?當然可以,不過它也有很多弊端。

比如程式設計師小吳的老式電腦處理10w資料使用氣泡排序要幾秒,但讀者的iMac Pro 可能只需要0.1s,這樣的結果誤差就很大了。更何況,有的演算法執行時間要很久,根本沒辦法沒時間去完整的執行,還是比如猴子排序:)。

那有什麼方法可以嚴謹的進行演算法的時間複雜度分析呢?

有的!

「 遠古 」的程式設計師大佬們提出了通用的方法:「 大O符號表示法 」,即 T(n) = O(f(n))

其中 n 表示資料規模 ,O(f(n))表示執行演算法所需要執行的指令數,和f(n)成正比。

上面公式中用到的 Landau符號是由德國數論學家保羅·巴赫曼(Paul Bachmann)在其1892年的著作《解析數論》首先引入,由另一位德國數論學家艾德蒙·朗道(Edmund Landau)推廣。Landau符號的作用在於用簡單的函式來描述複雜函式行為,給出一個上或下(確)界。在計算演算法複雜度時一般只用到大O符號,Landau符號體系中的小o符號、Θ符號等等比較不常用。這裡的O,最初是用大寫希臘字母,但現在都用大寫英語字母O;小o符號也是用小寫英語字母o,Θ符號則維持大寫希臘字母Θ。

注:本文用到的演算法中的界限指的是最低的上界。

常見的時間複雜度量級

我們先從常見的時間複雜度量級進行大O的理解:

  • 常數階O(1)

  • 線性階O(n)

  • 平方階O(n²)

  • 對數階O(logn)

  • 線性對數階O(nlogn)

看動畫輕鬆理解時間複雜度(一)

O(1)

看動畫輕鬆理解時間複雜度(一)

無論程式碼執行了多少行,其他區域不會影響到操作,這個程式碼的時間複雜度都是O(1)

void swapTwoInts(int &a, int &b){
  int temp = a;
  a = b;
  b = temp;
}
複製程式碼

O(n)

看動畫輕鬆理解時間複雜度(一)

在下面這段程式碼,for迴圈裡面的程式碼會執行 n 遍,因此它消耗的時間是隨著 n 的變化而變化的,因此可以用O(n)來表示它的時間複雜度。

int sum ( int n ){
   int ret = 0;
   for ( int i = 0 ; i <= n ; i ++){
      ret += i;
   }
   return ret;
}
複製程式碼

特別一提的是 c * O(n) 中的 c 可能小於 1 ,比如下面這段程式碼:

void reverse ( string &s ) {
    int n = s.size();
    for (int i = 0 ; i < n/2 ; i++){
      swap ( s[i] , s[n-1-i]);
    }
}
複製程式碼

O(n²)

看動畫輕鬆理解時間複雜度(一)
當存在雙重迴圈的時候,即把 O(n) 的程式碼再巢狀迴圈一遍,它的時間複雜度就是 O(n²) 了。

void selectionSort(int arr[],int n){
   for(int i = 0; i < n ; i++){
     int minIndex = i;
     for (int j = i + 1; j < n ; j++ )
       if (arr[j] < arr[minIndex])
           minIndex = j;
       
     swap ( arr[i], arr[minIndex]);
   }
}
複製程式碼

這裡簡單的推導一下

  • 當 i = 0 時,第二重迴圈需要執行 (n - 1) 次
  • 當 i = 1 時,第二重迴圈需要執行 (n - 2) 次
  • 。。。。。。

不難得到公式:

(n - 1) + (n - 2) + (n - 3) + ... + 0
= (0 + n - 1) * n / 2
= O (n ^2)

複製程式碼

當然並不是所有的雙重迴圈都是 O(n²),比如下面這段輸出 30n 次 Hello,五分鐘學演算法:)的程式碼。

void printInformation (int n ){
   for (int i = 1 ; i <= n ; i++)
        for (int j = 1 ; j <= 30 ; j ++)
           cout<< "Hello,五分鐘學演算法:)"<< endl;
}
複製程式碼

O(logn)

看動畫輕鬆理解時間複雜度(一)

int binarySearch( int arr[], int n , int target){
  int l = 0, r = n - 1;
  while ( l <= r) {
    int mid = l + (r - l) / 2;
    if (arr[mid] == target) return mid;
    if (arr[mid] > target ) r = mid - 1;
    else l = mid + 1;
  }
  return -1;
}
複製程式碼

在二分查詢法的程式碼中,通過while迴圈,成 2 倍數的縮減搜尋範圍,也就是說需要經過 log2^n 次即可跳出迴圈。

同樣的還有下面兩段程式碼也是 O(logn) 級別的時間複雜度。

  // 整形轉成字串
  string intToString ( int num ){
   string s = "";
   // n 經過幾次“除以10”的操作後,等於0
   while (num ){
    s += '0' + num%10;
    num /= 10;
   }
   reverse(s)
   return s;
  }
複製程式碼
void hello (int n ) {
   // n 除以幾次 2 到 1
   for ( int sz = 1; sz < n ; sz += sz) 
     for (int i = 1; i < n; i++)
        cout<< "Hello,五分鐘學演算法:)"<< endl;
}
複製程式碼

O(nlogn)

將時間複雜度為O(logn)的程式碼迴圈N遍的話,那麼它的時間複雜度就是 n * O(logn),也就是了O(nlogn)。

void hello (){
  for( m = 1 ; m < n ; m++){
    i = 1;
    while( i < n ){
        i = i * 2;
    }
   }
}


複製程式碼

下一節將深入的對遞迴演算法的複雜度進行分析,敬請期待:)

文章首發於公眾號:五分鐘學演算法

看動畫輕鬆理解時間複雜度(一)

相關文章