前端輕松學算法:時間復雜度


公眾號「前端小苑」,閱讀 時間復雜度詳解全文

 

通過學習本文,你可以掌握以下三點內容。
  1. 為什么需要時間復雜度

  2. 時間復雜度怎么表示

  3. 怎樣分析一段代碼的時間復雜度

 

相信認真閱讀過本文,面對一些常見的算法復雜度分析,一定會游刃有余,輕松搞定。文章中舉的例子,也盡量去貼近常見場景,難度遞增。

復雜度是用來衡量代碼執行效率的指標,直白講代碼的執行效率就是一段代碼執行所需要的時間。

那么,有人會問了,代碼執行所需要的時間,我執行一下,看看用了多少時間不就可以了?還要時間復雜度干啥?

 

一、為什么需要時間復雜度

實際開發時,我們希望自己的代碼是最優的,但總不能把所有的實現的方式都寫出來,再跑一遍代碼做比較,這樣不太實際,所以需要一個指標可以衡量算法的效率。
而且,直接跑代碼看執行時間會有一些局限性:

1.測試結果受當前運行環境影響

同樣的代碼在不同硬件的機器上進行測試,得到的測試結果明顯會不同。有時候同樣是a,b兩段代碼,在x設備上,a執行的速度比b快,但到了y設備上,可能會完全相反的結果。

2.測試結果受測試數據的影響

不同的測試數據可能會帶來不同的結果,比如我們采用順序查找的方法查找數組中的一個元素,如果這個元素剛好在數組的第一位,執行一次代碼就能找到,而如果要找的元素位於數組最后,就要遍歷完整個數組才能得到結果。

於是乎,我們需要一個不受硬件,宿主環境和數據集影響的指標來衡量算法的執行效率,它就是算法的復雜度分析。

 

二、時間復雜度怎么表示

我們知道了為什么需要時間復雜度,那要怎么來表示它呢?下面通過一個簡單例子,來說明一下 大O時間復雜度表示法。 首先看第一個例子:

1 function factorial(){ 2     let i = 0 // 執行了1次
3     let re = 1 // 執行了1次
4     for(;i < n; i++ ){ // 執行了n次
5         re*= n // 執行了n次
6  } 7     return re // 執行了1次
8 }

上面代碼是求n的階乘(n!)的方法,即n×...×3×2×1

我們粗略的計算一下這段代碼的執行時間。 首先,為了方便計算,假設執行每行代碼的執行時間是相同的。 在這里,假設每行代碼執行一次的時間設為t,代碼的總執行時間為T(n)。 代碼的第2行,第3行執行了一次,需要的時間是t + t = 2t; 代碼的第4行,第5行都執行了n次,所以用時(n + n)t = 2n*t; 代碼的第7行,只執行了一次,需要時間 t。

所以執行這段代碼的總時間是:

1 T(n) = 2t + 2nt + t = (2n + 3)t

我們以n為x軸,T(n)為y軸,繪制出坐標圖如下:

 

 
 
很明顯,代碼的總執行時間和每行代碼執行的次數成正比。大O時間復雜度表示法就是用來表示來這樣的趨勢。 大O表示法表示代碼執行時間隨數據規模增長的變化趨勢 下面是大O表示法的公式:
  T(n) = O(F(n)) 
  • n: **代表數據規模, 相當於上面例子中的n

  • F(n):表示代碼執行次數的總和,代碼執行次數的總和與數據規模有關,所以用F(n)表示, F(n)對應上面例子中的(2n+3)

  • T(n): 代表代碼的執行時間,對應上面例子中的T(n)

  • O: 大O用來表示代碼執行時間T(n) 與 代碼執行次數總和F(n)之間的正比關系。

現在已經知道了大O表示法公式的含義,我們嘗試着把上面例子得出的公式改寫成大O表示法,結果如下:

1 T(n) = O(2n + 3)

上面已經說過,大O表示法表示代碼執行時間隨數據規模增長的變化趨勢,只是表示趨勢,而不是代表實際的代碼執行時間, 當公式中的n無窮大時,系數和常數等對趨勢造成的影響就會微乎其微,可以忽略,所以,忽略掉系數和常數,最終上面例子簡化成如下的大O表示法:

1 T(n) = O(n)

至此,我們已經知道了什么是大O表示法以及如何使用大O表示法來表示時間復雜度,下面我們利用上面的知識,來分析下面代碼的時間復雜度。

 1 function reverse(arr){  2     let n = arr.length // 執行了1次
 3     let i = 0 // 執行了1次
 4     for(;i<n-1;i++){ // 執行了n次
 5         let j = 0 // 執行了n次
 6         for(j=0;j<n-1;j++){ // 執行了n²次
 7             var temp=arr[j] // 執行了n²次
 8             arr[j]=arr[j+1] // 執行了n²次
 9             arr[j+1]=temp // 執行了n²次
10  } 11  } 12 }

這段代碼的目的是顛倒數組中元素的順序 (代碼只是為了方便我們講解用,不需要考慮代碼是否最優),按照之前的分析方法分析,依然設每行代碼執行時間為t,代碼執行的總時間為T(n)。 第2,3行代碼只執行了1次,需要時間2t; 第4,5行代碼執行了n次,需要時間 2n*t 第6,7,8,9行代碼執行了n²次 所以,執行這段代碼的總時間:

1 T(n) = (4n² + 2n + 2)t

去除低階,系數和常數,最終使用大O表示法表示為:

1 T(n) = O(n²)

通過上面兩個例子,我們可以總結出用大O表示法表示時間復雜度的一些規律:

  1. 不保留系數

  2. 只保留最高階

  3. 嵌套代碼的復雜度內外代碼復雜度的乘積

 

三、如何快速分析一段代碼的時間復雜度

我們上面總結出了大O表示法的特點,只保留最高階,所以在分析代碼的時間復雜度時,我們只需要關心代碼執行次數最多的代碼,其他的都可以忽略。 還是看我們上面reverse的例子,執行次數最多的是代碼的6,7,8,9行,執行了n²次,我們就可以很容易計算出它的復雜度為大O(n²),這個方法同樣適用於存在判斷條件的代碼。下面是一段帶條件判斷語句的代碼:

 1 function test(n){  2     let res = 0
 3     if(n > 100){  4         const a = 1
 5         const b = 100
 6         res = (a + b)*100/2
 7     }else {  8         let i = 1
 9         for(;i<n; i++){ 10             res+=i 11  } 12  } 13     return res 14 }

這段代碼的含義是,當n > 100時, 直接返回1-100的和,n < 100時,返回1到n的和。 如果按照n的大小分開分析,當n > 100時,代碼的執行時間不隨 n 的增大而增長,其時間復雜度記為:

1 T(n) = O(1)

當n <= 100時,時間復雜度為:

1 T(n) = O(n)

上面n > 100的情況,是最理想的情況,這種最理想情況下執行代碼的復雜度稱為最好情況時間復雜度 n < 100時,是最壞的情況,在這種最糟糕的情況下執行代碼的時間復雜度稱為最壞情況時間復雜度 后面我們會有單獨的文章來分析最好情況時間復雜度,最壞時間復雜度,平均情況時間復雜度, 均攤時間復雜度。

除了特別說明,我們所說的時間復雜度都是指的最壞情況時間復雜度,因為只有在最壞的情況下沒有問題,我們對代碼的衡量才能有保證。所以我們這種情況,我們依然只需要關注執行次數最多的代碼,本例子的時間復雜度為O(n)。

為了方便我們確定哪段代碼在計算時間復雜度中占主導地位,熟悉常見函數的時間復雜度對比情況十分必要。

 

四、常見的時間復雜度

最常見的時間復雜度有常數階O(1),對數階O(logn),線性階O(n),線性對數階O(nlogn),平方階O(n²) 從下圖可以清晰的看出常見時間復雜度的對比:

 

 

1 O(1) < O(logn) < O(n) < O(nlogn) < O(n^2)

這些常見的復雜度,其中常數階O(1),線性階O(n),平方階O(n²)對我們來說已經不陌生了,在上文的例子中我們已經認識了他們,只有O(logn)還比較陌生,從圖中可見對數階的時間復雜度僅次於常數階,可以說是性能非常好了。下面就看一個復雜度是對數階的例子:

 1 function binarySearch(arr,key){  2     const n = arr.length  3     let low = 0
 4     let high = n - 1
 5     let mid = Math.floor((low + high) / 2)  6     while (low <= high) {  7         mid = Math.floor((low + high) / 2)  8         if (key === arr[mid]) {  9             return mid 10         } else if (key < arr[mid]) { 11             high = mid - 1
12         } else { 13             low = mid + 1
14  } 15  } 16     return -1
17 }

 

這是二分查找查找的代碼,二分查找是一個比較高效的查找算法。 現在,我們就分析下二分查找的時間復雜度。 這段代碼中執行次數最多的是第7行代碼,所以只需要看這段代碼的執行次數是多少。上面已經說過,我們現在考慮的都是最壞情況下的時間復雜度,那么對於這段代碼,最壞的情況就是一直排除一半,直到只剩下一個元素時才找到結果,或者要找的數組中不存在要找的元素。
現在已知,每次循環都會排除掉1/2不適合的元素,假設執行第T次后,數組只剩余1個元素。
那么:
第1次執行后,剩余元素個數 n/2
第2次執行后,剩余元素個數 n/2/2 = n/4
第3次執行后,剩余元素個數 n/2/2/2 = n/8
... ...
第n次執行后,剩余元素個數 n/(2^T) = 1

由公式 n/(2^T) = 1 可得 2^T = n, 所以總次數等於 log2n,使用大O表示法,省略底數,就是O(logn)

到這里為止,一段簡單的代碼時間復雜度就可以分析出來了。

更復雜的時間復雜度分析,以及最好情況、最壞情況、平均情況、均攤時間復雜度,將在后面文章中介紹。

 

 如果喜歡本文請掃描下方二維碼關注公眾號。更多系列原創文章,就在「前端小苑」。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM