輕鬆搞定時間複雜度

圖片描述

關注公衆號「前端小苑」,閱讀 時間複雜度詳解全文javascript

經過學習本文,你能夠掌握如下三點內容:前端

  1. 爲何須要時間複雜度
  2. 時間複雜度怎麼表示
  3. 怎樣分析一段代碼的時間複雜度

相信認真閱讀過本文,面對一些常見的算法複雜度分析,必定會遊刃有餘,輕鬆搞定。文章中舉的例子,也儘可能去貼近常見場景,難度遞增。java

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

那麼,有人會問了,代碼執行所須要的時間,我執行一下,看看用了多少時間不就能夠了?還要時間複雜度幹啥?數組

1、爲何須要時間複雜度

實際開發時,咱們但願本身的代碼是最優的,但總不能把全部的實現的方式都寫出來,再跑一遍代碼作比較,這樣不太實際,因此須要一個指標能夠衡量算法的效率。
並且,直接跑代碼看執行時間會有一些侷限性:函數

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

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

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

不一樣的測試數據可能會帶來不一樣的結果,好比咱們採用順序查找的方法查找數組中的一個元素,若是這個元素恰好在數組的第一位,執行一次代碼就能找到,而若是要找的元素位於數組最後,就要遍歷完整個數組才能獲得結果。學習

因而乎,咱們須要一個不受硬件,宿主環境和數據集影響的指標來衡量算法的執行效率,它就是算法的複雜度分析。測試

2、時間複雜度怎麼表示

咱們知道了爲何須要時間複雜度,那要怎麼來表示它呢?下面經過一個簡單例子,來講明一下 大O時間複雜度表示法。 首先看第一個例子:spa

function factorial(){
   let i = 0 // 執行了1次
   let re = 1 // 執行了1次
   for(;i < n; i++ ){ // 執行了n次
       re*= n // 執行了n次
   }
   return re // 執行了1次
}

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

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

因此執行這段代碼的總時間是:

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表示法,結果以下:

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

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

T(n) = O(n)

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

function reverse(arr){
      let n = arr.length // 執行了1次
      let i = 0 // 執行了1次
      for(;i<n-1;i++){ // 執行了n次
          let j = 0 // 執行了n次
          for(j=0;j<n-1;j++){ // 執行了n²次
              var temp=arr[j] // 執行了n²次
              arr[j]=arr[j+1] // 執行了n²次
              arr[j+1]=temp // 執行了n²次
         }
      }
  }

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

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

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

T(n) = O(n²)

經過上面兩個例子,咱們能夠總結出用大O表示法表示時間複雜度的一些規律:

1. 不保留係數

2. 只保留最高階

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

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

咱們上面總結出了大O表示法的特色,只保留最高階,因此在分析代碼的時間複雜度時,咱們只須要關心代碼執行次數最多的代碼,其餘的均可以忽略。 仍是看咱們上面reverse的例子,執行次數最多的是代碼的6,7,8,9行,執行了n²次,咱們就能夠很容易計算出它的複雜度爲大O(n²),這個方法一樣適用於存在判斷條件的代碼。下面是一段帶條件判斷語句的代碼:

function test(n){
      let res = 0
      if(n > 100){
          const a = 1
          const b = 100
          res = (a + b)*100/2
      }else {
          let i = 1
          for(;i<n; i++){
              res+=i
          }
      }
      return res
  }

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

T(n) = O(1)

當n <= 100時,時間複雜度爲:

T(n) = O(n)

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

除了特別說明,咱們所說的時間複雜度都是指的最壞狀況時間複雜度,由於只有在最壞的狀況下沒有問題,咱們對代碼的衡量纔能有保證。因此咱們這種狀況,咱們依然只須要關注執行次數最多的代碼,本例子的時間複雜度爲O(n)。

爲了方便咱們肯定哪段代碼在計算時間複雜度中占主導地位,熟悉常見函數的時間複雜度對比狀況十分必要。

4、常見的時間複雜度

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

圖片描述

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

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

function binarySearch(arr,key){
      const n = arr.length
      let low = 0
      let high = n - 1
      let mid = Math.floor((low + high) / 2)
      while (low <= high) {
          mid = Math.floor((low + high) / 2)
          if (key === arr[mid]) {
              return mid
          } else if (key < arr[mid]) {
              high = mid - 1
          } else {
              low = mid + 1
          }
      }
      return -1
  }

這是二分查找查找的代碼,二分查找是一個比較高效的查找算法。 如今,咱們就分析下二分查找的時間複雜度。 這段代碼中執行次數最多的是第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)

到這裏爲止,一段簡單的代碼時間複雜度就能夠分析出來了。

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

若是喜歡本文請掃描下方二維碼關注公衆號。更多原創系列文章,就在「前端小苑」。
圖片描述

相關文章
相關標籤/搜索