看動畫輕松理解時間復雜度(一)
算法(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(n2)
-
對數階O(logn)
-
線性對數階O(nlogn)
O(1)
無論代碼執行了多少行,其他區域不會影響到操作,這個代碼的時間復雜度都是O(1)
1void swapTwoInts(int &a, int &b){
2 int temp = a;
3 a = b;
4 b = temp;
5}
O(n)
在下面這段代碼,for循環裏面的代碼會執行 n 遍,因此它消耗的時間是隨著 n 的變化而變化的,因此可以用O(n)來表示它的時間復雜度。
1int sum ( int n ){
2 int ret = 0;
3 for ( int i = 0 ; i <= n ; i ++){
4 ret += i;
5 }
6 return ret;
7}
特別一提的是 c * O(n) 中的 c 可能小於 1 ,比如下面這段代碼:
1void reverse ( string &s ) {
2 int n = s.size();
3 for (int i = 0 ; i < n/2 ; i++){
4 swap ( s[i] , s[n-1-i]);
5 }
6}
O(n2)
當存在雙重循環的時候,即把 O(n) 的代碼再嵌套循環一遍,它的時間復雜度就是 O(n2) 了。
1void selectionSort(int arr[],int n){
2 for(int i = 0; i < n ; i++){
3 int minIndex = i;
4 for (int j = i + 1; j < n ; j++ )
5 if (arr[j] < arr[minIndex])
6 minIndex = j;
7
8 swap ( arr[i], arr[minIndex]);
9 }
10}
這裏簡單的推導一下
- 當 i = 0 時,第二重循環需要運行 (n - 1) 次
- 當 i = 1 時,第二重循環需要運行 (n - 2) 次
- 。。。。。。
不難得到公式:
1(n - 1) + (n - 2) + (n - 3) + ... + 0
2= (0 + n - 1) * n / 2
3= O (n ^2)
當然並不是所有的雙重循環都是 O(n2),比如下面這段輸出 30n 次 Hello,五分鐘學算法:)
的代碼。
1void printInformation (int n ){
2 for (int i = 1 ; i <= n ; i++)
3 for (int j = 1 ; j <= 30 ; j ++)
4 cout<< "Hello,五分鐘學算法:)"<< endl;
5}
O(logn)
1int binarySearch( int arr[], int n , int target){
2 int l = 0, r = n - 1;
3 while ( l <= r) {
4 int mid = l + (r - l) / 2;
5 if (arr[mid] == target) return mid;
6 if (arr[mid] > target ) r = mid - 1;
7 else l = mid + 1;
8 }
9 return -1;
10}
在二分查找法的代碼中,通過while循環,成 2 倍數的縮減搜索範圍,也就是說需要經過 log2^n 次即可跳出循環。
同樣的還有下面兩段代碼也是 O(logn) 級別的時間復雜度。
1 // 整形轉成字符串
2 string intToString ( int num ){
3 string s = "";
4 // n 經過幾次“除以10”的操作後,等於0
5 while (num ){
6 s += ‘0‘ + num%10;
7 num /= 10;
8 }
9 reverse(s)
10 return s;
11 }
1void hello (int n ) {
2 // n 除以幾次 2 到 1
3 for ( int sz = 1; sz < n ; sz += sz)
4 for (int i = 1; i < n; i++)
5 cout<< "Hello,五分鐘學算法:)"<< endl;
6}
O(nlogn)
將時間復雜度為O(logn)的代碼循環N遍的話,那麽它的時間復雜度就是 n * O(logn),也就是了O(nlogn)。
1void hello (){
2 for( m = 1 ; m < n ; m++){
3 i = 1;
4 while( i < n ){
5 i = i * 2;
6 }
7 }
8}
更多復雜度分析內容可以在公眾號 五分鐘學算法 獲取
看動畫輕松理解時間復雜度(一)