1. 程式人生 > >到底什麽是時間復雜度

到底什麽是時間復雜度

lan 不同 時間復雜度 fmt 會有 道路 出現 comment -s

?技術分享圖片

我們常常在武俠小說中看到一位內力精深的高手在學習新的招式的時候修煉速度異常驚人,我心目中最經典的片段就是倚天屠龍記中張無忌學習乾坤大挪移和太極拳的時候了,他能在極短的時間內領會常人數十年所不能掌握的東西,即使拍了很多版本,每次看到這,我都大呼過癮,仍然看的津津有味~

數據結構和算法對於程序員來說就像是武俠世界中江湖人士的內功心法,其重要程度不言而喻,而開啟數據結構與算法歷練之路大門的鑰匙則是復雜度的分析,這裏的復雜度主要指的就是時間復雜度。

數據結構與算法需要掌握的知識很多,後來經過大牛們的歸納總結提煉出圖中 10 種數據結構與 10 種常用的算法,只需要掌握下面這張圖我們日常工作中就可以遊刃有余了:

技術分享圖片

聰明的同學會發現其實圖中知識點還是很多的,看著少是因為沒有展開腦圖而已 ,要掌握的知識多是好事,說明我們進步的空間很大,學習之路可能很遠,沒關系,慢慢來,我們來日方長~

鋪墊了這麽多,相信大家對數據結構與算法也有了一些認識,我目前也是一名小白,期望通過每次的分享能夠在數據結構與算法的道路上走的更遠一些。

下面我們開始入門第一課 :時間復雜度的分析

主要包括以下 4 點:

  • 大O復雜度表示法

  • 常用的時間復雜度表示

  • 最好、最壞、平均、均攤時間復雜度

  • 空間復雜度

在一些面試題當中經常會出現對某一算法進行時間復雜度分析,在給出的選項中會有類似 O(1),O(n),O(logn)....的寫法,那麽像這樣的O()的寫法是什麽意思呢?

這就要提到時間復雜度分析常用的 大O復雜度表示法。

  • 大O復雜度

大O復雜度實際上並不具體代表代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,也叫漸進時間復雜度,簡稱為時間復雜度

看下面一個例子:

int sum(int n) {   int sum = 0;   int i = 1;   int j = 1;   for (; i <= n; ++i) {     j = 1;     for (; j <= n; ++j) {       sum = sum + i * j;     }   }}

對上述代碼而言,假設每一行代碼的執行時間都是相同的記為 time(time 為常量),第2-4 行代碼都是執行一次,各消耗時間為 time,第5、6行代碼各執行 n 次,各消耗時間為 n * time,第7、8 行代碼碼各執行 n*n 次,各消耗時間為 n*n * time,所以上述代碼的總消耗時間為:

2*n*n*time + 2*n*time+3*time = (2n2+2n+3)*time

用大 O 表示法則記為 O((2n2+2n+3)* time), 由於 time為常量,n為變量,即原式可以化簡為 O(2n2+2n+3)。

當n很大時,甚至可以認為它趨近於無窮大時,根據極限的知識我們可以認為 O(2n2+2n+3) 中低階、常量、系數三部分並不左右增長趨勢,所以可以忽略,只用記錄最高階就可以了,即 O(2n2+2n+3) 可以寫成 O(n2),這也就是上述代碼的時間漸進復雜度,簡稱時間復雜度

由上面的例子可以看出,其實在分析時間復雜度的時候,我們只要關心階數最高或者說是循環次數最多的一段代碼就可以了,因為我們一般會忽略低階、常量、系數這三部分。

時間復雜度常用到的另外2個法則是:加法法則和乘法法則,都比較簡單,這裏就不在多說了。

  • 常用的時間復雜度表示

O(1) , O(logn) , O(n) , O(nlogn) , O(n2),O(n3),O(2^n) ,O(n!),從左到右時間復雜度依次遞增

  • O(1)

O(1) 表示常量階,並不是說只執行了一行代碼,只要代碼的執行時間不隨著 n 的增大而增大就可以定為常量階,哪怕有成千上萬行代碼。另外如果有循環次數是常量的循環也定義常量階。

  • O(logn) 、O(nlogn)

這種對數階時間復雜度也是比較常見的。

int i = 1;while(i <= n){  i = i * 2;}

從代碼中可以看出 i 是成倍往上增長的,當 i 大於 n 時,循環就會結束,這其實是一道很簡單的對數題:

2^x = n, 求 x 的值

我們都知道 x = log2 n (以 2 為底 n 的對數),那時間復雜度為什麽不寫成 O(log2 n )呢?

其實寫成O(log2 n )也並沒有錯,如果我們把上述代碼第4行 改成 i = i * 3,那是不是要寫成 O(log3 n )了,顯然不是,這樣寫雖然不錯但是太麻煩了。

由我們僅存的高中數學知識可知,對數是可以相互轉化的 :

log3n = log32 * log2n,log32為常數

那麽,O(log3n) = O (log32 * log2n)= O(log2n),所以在對數時間復雜度中,就可以忽略對數的底,統一標識為 O(logn)

而 nO(logn) 就顯而易見了,根據乘法法則,在外層再套一層時間復雜度為 O (n)的循環,上述代碼復雜度就是 nO(logn) 了。

歸並排序、快速排序的時間復雜度都是O(nlogn)。

  • 最好、最壞、平均、均攤時間復雜度

在了解常用的復雜度後,我們在深入一層,之前的代碼都比較簡單,不需要考慮相應的情況,下面我們看一個比較復雜的例子:

//數組中查找變量 target 的位置,有則返回下標,沒有則返回1int find(int[] array, int n, int target) {  int i = 0;  int pos = -1;  for (; i < n; ++i) { // n表示數組array的長度    if (array[i] == target) {        pos = i;        break;      }  }  return pos;}

上面這段代碼的時間復雜度是多少呢?

這個時候我們可能會有這樣的疑問:

目標值在不在數組中,如果不在怎麽辦,如果在,那具體在哪個位置呢?

這裏我們如果再用之前的方法分析,結果就會有偏差了,因為代碼中的循環是有可能被中斷的(當找到目標值後 break)此時引入最好時間復雜度、最壞時間復雜度、平均時間復雜度的概念了。

顧名思義,最好時間復雜度就是最理想的情況下,也就是目標值就是數組的第一個元素,此時對應的時間復雜度為 O(1)

最壞時間復雜度就是最差的情況下,也就是說目標值不在數組中(或目標值在數組的末尾),此時需要循環n次中才能知道目標值是否在數組中,對應時間復雜度為O(n)。

  • 平均時間復雜度

最好和最壞時間復雜度其實都是極特殊的情況,為了更好的解釋平均時間復雜度需要引入一個概念:平均情況時間復雜度,後面簡稱為平均時間復雜度

所謂的平均情況與求平均值類似,上述的尋找目標值在數組中的位置,一共有n+1 中情況,包括在數組 0 ~ n-1 的任一下標上 和不在數組中的情況,把需要查找元素的個數累加起來,再除以 n+1 ,就可以得到遍歷元素的平均值:

(1+2+3+4……+n+n)/(n+1) = n(n+3)/2(n+1)

將O (n(n+3)/2(n+1)) 根據上述規則轉換後,可以得出平均時間復雜度為 O(n)。

  • 加權平均時間復雜度

我們雖然得出了結果,但是仔細一想這個結果好像並不準確,原因在於 這 n+1 種情況出現的概率其實是不同的,而且目標值出現在數組中某一位置的概率也是不同的。

為了方便計算,我們假定目標值出現在數組中與不在數組中的概率是相等的,都為 1/2 ;目標值出現在數組中某一位置的概率也是相等的,都為 1/n,根據乘法法則,我們要找的目標值出現在數組中的概率應該為 (1/2) * (1/n), 1/(2n)。

所以前面的計算最大的問題是沒有考慮概率問題,將概率添加上的算式為:

((1+2+3+4……+n)*(1/2n)+ n*(1/2) ) = (3n+1)/4

這個值就是概率論中的加權平均值,也叫作期望值,所以平均時間復雜度的全稱應該叫加權平均時間復雜度或者期望時間復雜度。

實際上一般情況下我們並不區分最好、最差、平均時間復雜度這三種情況。使用最開始的一個復雜度就可以滿足需求了。

如果出現一塊代碼在不同情況下,時間復雜度有重量級差距,才會使用這三種復雜度來區分。

  • 均攤時間復雜度

均攤時間復雜度應用場景比較特殊,所以我們並不會經常用到。

均攤時間復雜度的主要思想是:

對一個數據結構進行一組連續操作中,大部分情況下時間復雜度都很低,只有個別情況下時間復雜度比較高,而且這些操作之間存在前後連貫的時序關系,這個時候,我們就可以將這一組操作放在一塊兒分析,看是否能將較高時間復雜度那次操作的耗時,平攤到其他那些時間復雜度比較低的操作上。而且,在能夠應用均攤時間復雜度分析的場合,一般均攤時間復雜度就等於最好情況時間復雜度 。

  • 空間復雜度

在了解了時間復雜度後,最後再補充一下空間復雜度,其實空間復雜度很簡單,它表示算法的存儲空間與時間的增長關系。

一般重用的空間復雜度就是 O(1)、 O(n)、 O(n2 ),像O(logn)、 O(nlogn)這樣的對數階復雜度平時都用不到。

通過今天的分享,我們主要了解了數據結構與算法的重要性,與時間復雜度相關的一些知識,之後我們會繼續學習數據結構與算法相關的知識,一起修煉內功,成為“江湖中的大俠”。

到底什麽是時間復雜度