一套圖 搞懂“時間複雜度”

寫在前面:

這篇文章是在公衆號: 程序員小灰 中發佈的。是我到目前爲止所看到的關於時間複雜度介紹的最好的文章,簡介 清晰 明瞭。

所以拿來po出來 僅供學習交流,如侵則刪。

現已將此文收錄至: 《數據結構》C語言版 (清華嚴蔚敏考研版) 全書知識梳理


正文: 

 

 

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

 

640?wx_fmt=png

時間複雜度的意義

 

究竟什麼是時間複雜度呢?讓我們來想象一個場景:某一天,小灰和大黃同時加入了一個公司......

640?wx_fmt=jpeg

一天過後,小灰和大黃各自交付了代碼,兩端代碼實現的功能都差不多。大黃的代碼運行一次要花100毫秒,內存佔用5MB。小灰的代碼運行一次要花100秒,內存佔用500MB。於是......

640?wx_fmt=jpeg

640?wx_fmt=jpeg

由此可見,衡量代碼的好壞,包括兩個非常重要的指標:

1.運行時間;

2.佔用空間。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

 

640?wx_fmt=png

基本操作執行次數

 

關於代碼的基本操作執行次數,我們用四個生活中的場景,來做一下比喻:

場景1:給小灰一條長10寸的麪包,小灰每3天吃掉1寸,那麼吃掉整個麪包需要幾天?

640?wx_fmt=jpeg

答案自然是 3 X 10 = 30天。

如果麪包的長度是 N 寸呢?

此時吃掉整個麪包,需要 3 X n = 3n 天。

如果用一個函數來表達這個相對時間,可以記作 T(n) = 3n。

場景2:給小灰一條長16寸的麪包,小灰每5天吃掉麪包剩餘長度的一半,第一次吃掉8寸,第二次吃掉4寸,第三次吃掉2寸......那麼小灰把麪包吃得只剩下1寸,需要多少天呢?

這個問題翻譯一下,就是數字16不斷地除以2,除幾次以後的結果等於1?這裏要涉及到數學當中的對數,以2位底,16的對數,可以簡寫爲log16。

因此,把麪包吃得只剩下1寸,需要 5 X log16 = 5 X 4 = 20 天。

如果麪包的長度是 N 寸呢?

需要 5 X logn = 5logn天,記作 T(n) = 5logn。

場景3:給小灰一條長10寸的麪包和一個雞腿,小灰每2天吃掉一個雞腿。那麼小灰吃掉整個雞腿需要多少天呢?

640?wx_fmt=jpeg

答案自然是2天。因爲只說是吃掉雞腿,和10寸的麪包沒有關係 。

如果麪包的長度是 N 寸呢?

無論麪包有多長,吃掉雞腿的時間仍然是2天,記作 T(n) = 2。

場景4:給小灰一條長10寸的麪包,小灰吃掉第一個一寸需要1天時間,吃掉第二個一寸需要2天時間,吃掉第三個一寸需要3天時間.....每多吃一寸,所花的時間也多一天。那麼小灰吃掉整個麪包需要多少天呢?

答案是從1累加到10的總和,也就是55天。

如果麪包的長度是 N 寸呢?

此時吃掉整個麪包,需要 1+2+3+......+ n-1 + n = (1+n)*n/2 = 0.5n^2 + 0.5n。

記作 T(n) = 0.5n^2 + 0.5n。

640?wx_fmt=jpeg

上面所講的是吃東西所花費的相對時間,這一思想同樣適用於對程序基本操作執行次數的統計。剛纔的四個場景,分別對應了程序中最常見的四種執行方式:

場景1:T(n) = 3n,執行次數是線性的。


 
  1. void eat1(int n){

  2.     for(int i=0; i<n; i++){;

  3.         System.out.println("等待一天");

  4.         System.out.println("等待一天");

  5.         System.out.println("吃一寸麪包");

  6.     }

  7. }

  8. vo

場景2:T(n) = 5logn,執行次數是對數的。


 
  1. void eat2(int n){

  2.    for(int i=1; i<n; i*=2){

  3.        System.out.println("等待一天");

  4.        System.out.println("等待一天");

  5.        System.out.println("等待一天");

  6.        System.out.println("等待一天");

  7.        System.out.println("吃一半面包");

  8.    }

  9. }

場景3:T(n) = 2,執行次數是常量的。


 
  1. void eat3(int n){

  2.    System.out.println("等待一天");

  3.    System.out.println("吃一個雞腿");

  4. }

場景4:T(n) = 0.5n^2 + 0.5n,執行次數是一個多項式。


 
  1. void eat4(int n){

  2.    for(int i=0; i<n; i++){

  3.        for(int j=0; j<i; j++){

  4.            System.out.println("等待一天");

  5.        }

  6.        System.out.println("吃一寸麪包");

  7.    }

  8. }

 

640?wx_fmt=png

漸進時間複雜度

 

有了基本操作執行次數的函數 T(n),是否就可以分析和比較一段代碼的運行時間了呢?還是有一定的困難。

比如算法A的相對時間是T(n)= 100n,算法B的相對時間是T(n)= 5n^2,這兩個到底誰的運行時間更長一些?這就要看n的取值了。

所以,這時候有了漸進時間複雜度(asymptotic time complexity)的概念,官方的定義如下:

若存在函數 f(n),使得當n趨近於無窮大時,T(n)/ f(n)的極限值爲不等於零的常數,則稱 f(n)是T(n)的同數量級函數。

記作 T(n)= O(f(n)),稱O(f(n)) 爲算法的漸進時間複雜度,簡稱時間複雜度。

漸進時間複雜度用大寫O來表示,所以也被稱爲大O表示法。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

如何推導出時間複雜度呢?有如下幾個原則:

  1. 如果運行時間是常數量級,用常數1表示;

  2. 只保留時間函數中的最高階項;

  3. 如果最高階項存在,則省去最高階項前面的係數。

讓我們回頭看看剛纔的四個場景。

場景1:

T(n) = 3n 

最高階項爲3n,省去係數3,轉化的時間複雜度爲:

T(n) =  O(n)

640?wx_fmt=png

場景2:

T(n) = 5logn 

最高階項爲5logn,省去係數5,轉化的時間複雜度爲:

T(n) =  O(logn)

640?wx_fmt=png

場景3:

T(n) = 2

只有常數量級,轉化的時間複雜度爲:

T(n) =  O(1)

640?wx_fmt=png

場景4:

T(n) = 0.5n^2 + 0.5n

最高階項爲0.5n^2,省去係數0.5,轉化的時間複雜度爲:

T(n) =  O(n^2)

640?wx_fmt=png

這四種時間複雜度究竟誰用時更長,誰節省時間呢?稍微思考一下就可以得出結論:

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

在編程的世界中有着各種各樣的算法,除了上述的四個場景,還有許多不同形式的時間複雜度,比如:

O(nlogn), O(n^3), O(m*n),O(2^n),O(n!)

今後遨遊在代碼的海洋裏,我們會陸續遇到上述時間複雜度的算法。

640?wx_fmt=png

 

640?wx_fmt=png

時間複雜度的巨大差異

 

 

640?wx_fmt=jpeg

640?wx_fmt=jpeg

我們來舉過一個栗子:

算法A的相對時間規模是T(n)= 100n,時間複雜度是O(n)

算法B的相對時間規模是T(n)= 5n^2,時間複雜度是O(n^2)

算法A運行在小灰家裏的老舊電腦上,算法B運行在某臺超級計算機上,運行速度是老舊電腦的100倍。

那麼,隨着輸入規模 n 的增長,兩種算法誰運行更快呢?

640?wx_fmt=png

從表格中可以看出,當n的值很小的時候,算法A的運行用時要遠大於算法B;當n的值達到1000左右,算法A和算法B的運行時間已經接近;當n的值越來越大,達到十萬、百萬時,算法A的優勢開始顯現,算法B則越來越慢,差距越來越明顯。

這就是不同時間複雜度帶來的差距。

640?wx_fmt=jpeg

發佈了66 篇原創文章 · 獲贊 9 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章