煩不煩,別再問我時間複雜度了:這次不色,女孩子進來吧

 

相關歷史文章(閱讀本文之前,您可能需要先看下之前的系列👇

色談Java序列化:女孩子慎入 - 第280篇

 

內心世界:前面一篇文章,度沒控制好,差點就變成 黃色編程 了,這篇應該怎麼寫呢,不要毀了我帥氣的形象

悟纖:師傅,徒兒最近在研究算法的時候,研究完之後,都需要通過運行程序來檢測算法的性能。有些算法運行需要半小時,嚴重影響了我這學習的速度了。有沒有辦法我不想要運行程序就可以預估可能執行的“時間”。

師傅:徒兒,真好學,對於這個運行時間,或者程序的性能的話,還真有一個指標可以去衡量,那就是時間複雜度

悟纖:時間複雜度,這是什麼東東呢?

師傅:徒兒別急,待爲師給你好好講解一番。

BTW:算法的複雜度分爲時間複雜度和空間複雜度,時間複雜度是指衡量算法執行時間的長短;空間複雜度是指衡量算法所需存儲空間的大小。

 

一、why:爲什麼要使用時間複雜度

一個算法在證明數學正確性後,我們要關心它的運行時間,這是一個程序性能的重要指標。

       師傅:程序的運行時間如何得到呢?

       悟纖:這個還不簡單,在代碼開始之前得到開始時間,在代碼結束的時候得到結束時間,通過(結束時間-開始時間),這不就得到了運行時間了嘛。

       師傅:那這樣子是不是勢必就要運行程序才能得到這個運行時間呢?如果都是能夠快速運行完的代碼,這樣子也不錯,能夠精確的得到代碼的執行時間,如果是一個複雜的算法需要運行的比較久的話,那麼這個時候就會比較痛苦了,修改一下算法就需要再運行一下。

 

       所以通過實際運行得到算法的時間的話,有這麼幾個小缺點:

(1)複雜的算法通過開發到運行後在又優化,流程會很長,整體操作時間長。

(2)運行時間受硬件、軟件的影響,這對我們評估算法本身存在影響。

       我們是否可以做到在運行前,或者在編寫前就預估出可能執行的“時間”。

這時候時間複雜度就孕育而生了,時間複雜度不是計算算法運行時間,而是估算出算法的複雜度,是個量級的概念。我們可以通過可能出現的時間複雜度,來選擇可以接受的算法。

BTW:通過時間複雜度來預估算法的複雜程度,並不能夠計算算法的運行時間。

 

二、what:什麼是時間複雜度

2.1 概念

在引入時間複雜度的概念的時候,我們需要先來了解另外一個概念時間頻度:

時間頻度 一個算法執行所耗費的時間,從理論上是不能算出來的,必須上機運行測試才能知道。但我們不可能也沒有必要對每個算法都上機測試, 只需知道哪個算法花費的時間多,哪個算法花費的時間少就可以了。並且一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱爲語句頻度或時間頻度。記爲T(n)。

       瞭解了時間頻度之後,就可以來給時間複雜度下個定義了:

時間複雜度 在剛纔提到的時間頻度中,n稱爲問題的規模,當n不斷變化時,時間頻度T(n)也會不斷變化。但有時我們想知道它變化時呈現什麼規律。爲此,我們引入時間複雜度概念。一般情況下,算法中基本操作重複執行的次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限值爲不等於零的常數,則稱f(n)是T(n)的同數量級函數。記作T(n)=O(f(n)),稱O(f(n))爲算法的漸進時間複雜度,簡稱時間複雜度。

BTW

(1)在概念中要求T(n)/f(n)的極限值爲不等於零的常數,這個常數不妨理解爲O(大O,不是零),那麼等式就是T(n)/f(n) = O,變型一下就是爲T(n) = O( f(n) ) ,這個等式我們一般這麼讀,T(n)的時間複雜度爲O(f(n))。

(2)n 爲算法使用者可以傳入的變量,通常時間複雜度受該參數影響。

(3)T(n) 算法的運行次數,次數隨着 n 的變化,而變化。

(4)O(f(n)) 算法運行次數變化的規律,也就是時間複雜度,以大寫的 O 爲符號標記。

(5)f(n) 時間複雜度的值,是個近似值。

最壞時間複雜度:

最壞情況下的時間複雜度稱最壞時間複雜度。一般不特別說明,討論的時間複雜度均是最壞情況下的時間複雜度。這樣做的原因是:最壞情況下的時間複雜度是算法在任何輸入實例上運行時間的上界,這就保證了算法的運行時間不會比任何更長。

 

2.2 舉個栗子

       

            我們先通過一些小栗子來理解這個時間頻度和時間複雜度吧。

「以下代碼是JS代碼」

栗子1:

console.log("hello,悟纖");

執行一次console.log我們進行一次運算,那麼T(n) = 1,這個算法的時間複雜度就是O(1),也稱爲常數階。

栗子2:

for(var i =0; i < n; i++){
     console.log(i);
}

這個console.log需要執行n次,那麼T(n) = n。隨着參數 n的變化而變化,那麼這個算法的時間複雜度爲 O(n),也稱爲線性階。

栗子3:

for(var i =0; i < n; i++){
    for(var j =0; j < n; j++){
        console.log(i + j);
    }
}

 

在這個算法裏,console.log(i+ j); 的執行次數爲 n * n,那麼T(n) = n²,時間複雜度就是O(n²),也成爲平方階。

       通過這幾個例子,我們可以得到計算時間複雜度的三個步驟

(1)找出算法的基本運行語句

(2)計算運行次數的量級

(3)使用 O 將其標記起來

       通過上面的例子中,會有一種誤區就是O( f(n) ) 中的f(n) 就等於T(n),這是錯誤的。

那麼時間複雜度是如何計算的吶?

 

三、how:如何計算時間複雜度

3.1 計算方式

計算時間複雜度也就是計算函數 f(n) 的值,是一個量級,在複雜算法中,時間複雜度關心的是最大的量級。

計算方式有如下規則:

(1)不受參數 n 影響的運算次數,我們用常量 C 表示,當算法有參數n 時,C 可以忽略不計,否則用 1 代替。(常數變1,然後去常數,去常參)。

(2)不受 for 循環影響的運算次數,使用加減法計算,否則使用乘法計算。

(3)在最後的計算公式中,我們使用最大量級的值,來代表整個算法的時間複雜度。(去低階)

   有點抽象吧,還是舉例說明。

3.2 舉個栗子

栗子:常量變 1

console.log("hello,悟纖");

console.log("hello,師傅");

console.log("hello,八戒");

公式推導如下:

f(n) = Θ(1 + 1 + 1) = 1 # Θ 表示常量變1、去常數、去常參、去低階
T(n) = O(f(n)) = O(1)

所以上面的算法時間複雜度爲:O(1)

 

栗子:去常數

 

console.log("Hello World");     // 1

console.log("Hello World");     // 1

for(var i =0; i < n; i++){     // n

    console.log("HelloWorld"); // 1

}

 

公式推導如下↓↓↓:

f(n) = Θ(1 + 1 + n * 1) 
     = Θ(2 + n) 
     = n
T(n) = O(f(n)) = O(n)

所以上面的算法時間複雜度爲:O(n)

BTW:爲什麼可以去掉常量?當 n 趨近無窮大時,常亮對最終的結果來說已經是無足輕重了,時間複雜度只關心最大的量級,所以常量可以忽略不計。

 

栗子:去常參

 

console.log("Hello World");         // 1

for(var i =0; i < n; i++){         // n

    console.log("Hello World ");    // 1

}

for(var i =0; i < n; i++){         // n

    console.log("Hello World ");    // 1

}

公式推導如下↓↓↓:

f(n) = Θ(1 + n * 1 + n * 1)      = Θ(1 + 2n)      = nT(n) = O(f(n))      = O(n)

所以上面算法的時間複雜度爲:O(n)。

BTW:當n趨近無限大的時候,n前面的係數,對於結果就影響比較小,可以n前面的係數就可以忽略不計。

 

栗子:去低階

for(var i =0; i < n; i++){         // n

    console.log("Hello World ");    // 1

}

for(var i =1; i < n; i++){         // n - 1

    for(var j =1; j < n; j++){     // n - 1

        console.log("Hello World ");// 1

    }

}

公式推導如下↓↓↓:

f(n) = Θ(n * 1 + (n - 1) * (n - 1) * 1) 
     = Θ(n + n * n) = Θ(n +n^2) 
     = n^2T(n) = O(f(n)) 
     = O(n²)

那麼上面算法的時間複雜度就是:O(n²)

BTW:爲什麼可以去低階? 同樣的道理,當 n 趨近無窮時,n 在 n^2 的量級面前不值一提,所以我們可以去低階。

 

四、常見的時間複雜度

 

常用時間複雜度所耗費時間從小到大依次爲:

 

在上圖中,我們可以看到當 n 很小時,函數之間不易區分,很難說誰處於主導地位,但是當 n 增大時,我們就能看到很明顯的區別,誰是老大一目瞭然:

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

 

五、其它要點

5.1 時間頻度不同,時間複雜度可能相同

舉例說明↓↓↓:

T(n)=n2+3n+4與T(n)=4n2+2n+1它們的頻度不同,但時間複雜度相同,都爲O(n2)。

5.2 複雜度默認指的就是時間複雜度

通常沒有特別指明時,複雜度指的是時間複雜度,我們寫代碼時,要學會以空間來換取時間。

 

六、題外話

      我們來看一下對數階的推導過程,代碼如下:

var i =1;

while(i <= n){

   i = i *2;

}

代碼解讀:

n是一個不確定的數,有一個while循環,結束的條件是i<=n的值,在循環體內 i的值是2倍的增加。

時間複雜度推導:

對於:var i=1,代碼執行一次,那麼關鍵是循環體的while循環需要執行多少次,決定了算法的時間複雜度。

這個while循環到底需要運行多少次呢?這個是未知數,我們使用變量k來表示,那麼通過循環的結束條件i<=n的時候,循環結束,可以得到一個公式,我們看一下具體的推導過程:

1<=n  // 執行1次判斷,0次循環體

1*2<=n  // 執行2次判斷,1次循環體

1*2*2<=n  // 執行3次判斷,2次循環體

1*2*2*2<=n  // 執行4次判斷,3次循環體

….

當假設循環體需要執行k次的時候,那麼循環體也就是k-1次了,

那麼就可以推導得到如下等式:

1*2^(k-1)=2^(k-1)<=n

通過這個等式就可以推導出來k的值爲:

k<=log(2)(n)+1,最大值就是k=log(2)(n)+1

這時候就可以計算得到時間頻度T(n)= 1+log(2)(n)+1。

那麼f(n)=θ( 1+log(2)(n)+1 )      = log(2)(n)時間複雜度:
T(n) = O(f(n))    = O(log(2)(n))

       通過以上分析,上面的算法的時間複雜度爲:O( log(2)(n) ) 。【log(2)(n)表示以2爲底n的對數】

BTW:對數公式是數學中的一種常見公式,如果a^x=N(a>0,且a≠1),則x叫做以a爲底N的對數 , 記做x=log(a)(N)。

 

 

七、悟纖小結

       師傅:爲師今天講了很多,悟纖,來,你給大家做個總結吧。

(1)爲什麼需要時間複雜度:通過時間複雜度可以來預估算法的複雜程度。

(2)時間頻度:算法運行次數就是時間頻度,使用T(n) 表示,舉例說明:n的雙層for循環,那麼T(n) = n²。

(3)時間複雜度:算法運行次數變化的規律就是時間複雜度,使用O(f(n)) 來表示,舉例說明:雙層for的f(n) = Θ( n² ) = n² ,所以T(n) = n²的時間複雜度就是O(n²)。

(4)時間複雜度計算規則:常量取1「T(n) = C : O(1)」;n碰到常數,去常數「T(n)=n+c:O(n)」;n前係數,直接去「T(n)=cn : O(n)」; 高階碰低階,底階靠邊站 「 T(n) =n²+n:O(n²) 」。(複雜一些的時間複雜度是需要通過計算才能進行推導出來的)

師傅:師傅累壞了,得去打坐下了,徒兒爲我護法下。

悟纖:師傅,你這就去好好休息下,徒兒在,妖怪豈敢放肆。

我就是我,是顏色不一樣的煙火。
我就是我,是與衆不同的小蘋果。

à悟空學院:https://t.cn/Rg3fKJD 

學院中有Spring Boot相關的課程!點擊「閱讀原文」進行查看!

SpringBoot視頻:https://t.cn/R3QepWG

Spring Cloud視頻:https://t.cn/R3QeRZc

SpringBoot Shiro視頻:https://t.cn/R3QDMbh  

SpringBoot交流平臺:https://t.cn/R3QDhU0

SpringData和JPA視頻:https://t.cn/R1pSojf

SpringSecurity5.0視頻:https://t.cn/EwlLjHh

Sharding-JDBC分庫分表實戰:https://t.cn/E4lpD6e

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章