1. 程式人生 > 實用技巧 >【進階之路】演算法的時間複雜度與空間複雜度

【進階之路】演算法的時間複雜度與空間複雜度

大家好,我是練習java兩年半時間的南橘,從一名連java有幾種資料結構都不懂超級小白,到現在懂了一點點的進階小白,學到了不少的東西。知識越分享越值錢,我這段時間總結(包括從別的大佬那邊學習,引用)了一些平常學習和工作中的重點(自我認為),希望給大家帶來一些幫助


因為最近在學習軟體設計師、正巧遇上了概念性的演算法題。因為之前學習並不系統的原因,雖然能做題,但是卻不是非常瞭解演算法中時間複雜度。本著研究學習的心理,這幾天就開始研究演算法中的時間複雜度,還真學到了一些東西。

一、時間複雜度

在電腦科學中,時間複雜性,又稱時間複雜度,演算法的時間複雜度是一個函式,它定性描述該演算法的執行時間。這是一個代表演算法輸入值的字串的長度的函式。時間複雜度常用大O符號表述,不包括這個函式的低階項和首項係數。使用這種方式時,時間複雜度可被稱為是漸近的,亦即考察輸入值大小趨近無窮時的情況。

  • 1、時間頻度 一個演算法執行所耗費的時間,從理論上是不能算出來的,必須上機執行測試才能知道。但是我們沒有要對每個演算法都上機測試。有經驗的程式設計師只需看一看就能知道哪個演算法花費的時間多,哪個演算法花費的時間少就可以了。並且一個演算法花費的時間與演算法中語句的執行次數成正比例,哪個演算法中語句執行次數多,它花費時間就多。一個演算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)。
  • 2、時間複雜度 在剛才提到的時間頻度中,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)) 為演算法的漸進時間複雜度,簡稱時間複雜度。

Landau符號(大O符號)其實是由德國數論學家保羅·巴赫曼(Paul Bachmann)在其1892年的著作《解析數論》首先引入,由另一位德國數論學家艾德蒙·朗道(Edmund Landau)推廣。Landau符號的作用在於用簡單的函式來描述複雜函式行為,給出一個上或下(確)界。在計算演算法複雜度時一般只用到大O符號,Landau符號體系中的小o符號、Θ符號等等比較不常用。這裡的O,最初是用大寫希臘字母,但現在都用大寫英語字母O;小o符號也是用小寫英語字母o,Θ符號則維持大寫希臘字母Θ。

所以,在其他條件不變的情況下,選擇時間複雜度低的演算法更有利於提高程式的效率。大家不要覺得演算法這東西沒有用,覺得是頂尖的程式設計師才用得到。其實我們在工作中,要經常根據情況新增許多工具類,這些解決具體問題的工具類往往又要涉及到遞迴、迴圈、排序、動態規劃等問題。對於我們來說,能夠實現功能就夠了,但是如果能進一步地降低這些工具類的時間複雜度,或許能讓我們感覺到自己的價值吧。

二、時間複雜度

常見的時間複雜度有:常數階O(1),對數階O(log2n),線性階O(n), 線性對數階O(nlog2n),平方階O(n²), k次方階O(n^k),指數階O(2^n)。隨著問題規模n的不斷增大,上述時間複雜度不斷增大,演算法的執行效率越低。

1、常數階O(1)

int a = 1;
int b = 2;
int c = 3;

我們假定每執行一行程式碼所需要消耗的時間為1個時間單位,那麼以上3行程式碼就消耗了3個時間單位。O(1)的1代表的是常數,常數階的演算法的複雜度是不會隨著問題規模的增大而增大,這樣的程式碼不管有多少行,都可以用O(1)來表示它的時間複雜度。

2、對數階O(logN)

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

從數學上我們可以很簡單的看出它的函式:

2^f(n)<=n,所以f(n)<=log2n

每次迴圈的時候 i都會乘2,那麼總共迴圈的次數就是log2n,因此這個程式碼的時間複雜度為O(log2n)。但是,底數如何對於程式執行的效率來說並不重要,就和之前的常數階一樣,常數部分則忽略,同樣的,如果不同時間複雜度的倍數關係為常數,那也可以近似認為兩者為同一量級的時間複雜度。

如果這樣不好理解,我們可以用二叉樹來表示。如果二叉樹的是以紅黑樹等平衡二叉樹實現的,則n個節點的二叉排序樹的高度為 log2n+1 ,其查詢效率為O(Log2n),近似於折半查詢。

3、線性階O(n)

for(i = 0; i <n; i++) {
   int a =1;
   int b=1;
   int c =a+b;
}

這段程式碼會執行多少次呢?如果從程式碼上來看,每一行都會執行n次(或者n-1次),所以最後會執行 T(n)=n+3(n-1)=4n-3次。
我們知道:大O符號表示法並不是用於來真實代表演算法的執行時間的,它是用來表示程式碼執行時間的增長變化趨勢的。所以線性階O(n)的時間複雜度其實是O(n);

4、線性對數階O(nlogN)

for(m = 1; m < n; m++) {
    i = 1;
    while(i < n) {
        i = i * 2;
    }
}

線性對數階O(nlogN) 就非常非常容易理解了,將時間複雜度為O(logn)的程式碼迴圈N遍的話,那麼它的時間複雜度就是 n*O(logN)。

5、平方階O(n²)

for(i = 1; i <= n; i++){
   for(j = 1; j <= n; j++) {
       j = i;
       j++;
    }
}

把 O(n) 的程式碼再巢狀迴圈一遍,它的時間複雜度就是 O(n²) 了。

6、立方階O(n³)、K次方階O(n^k)

參考上面的O(n²) 去理解就好了,O(n³)相當於三層n迴圈,O(n^k)就是k層迴圈。

三、空間複雜度

一個程式的空間複雜度是指執行完一個程式所需記憶體的大小。與時間複雜度相類似的,利用程式的空間複雜度,可以對程式的執行所需要的記憶體多少有個預先估計一個程式執行時除了需要儲存空間和儲存本身所使用的指令、常數、變數和輸入資料外,還需要一些對資料進行操作的工作單元和儲存一些為現實計算所需資訊的輔助空間。程式執行時所需儲存空間包括以下兩部分。

-1 、固定部分:這部分空間的大小與輸入/輸出的資料的個數多少、數值無關,主要包括指令空間(即程式碼空間)、資料空間(常量、簡單變數)等所佔的空間,這部分屬於靜態空間。

-2 、可變空間:這部分空間的主要包括動態分配的空間,以及遞迴棧所需的空間等,這部分的空間大小與演算法有關。一個演算法所需的儲存空間用f(n)表示。S(n)=O(f(n)),其中n為問題的規模,S(n)表示空間複雜度。

1、空間複雜度 O(1)

int i = 1;
int j = 1;
int k = i + j;

如果演算法執行所需要的臨時空間不隨著某個變數n的大小而變化,即此演算法空間複雜度為一個常量,可表示為 O(1)。
i、j、k所分配的空間都不隨著處理資料量變化,因此它的空間複雜度 S(n) = O(1)。

2、空間複雜度 O(n)

int[] m = new int[n]
for(i = 0; i <n; i++) {
   int a =1;
   int b=1;
   int c =a+b;
}

這段程式碼的第一行new了一個數組出來,這個資料佔用的大小為n,後面雖然有迴圈,但沒有再分配新的空間,因此,這段程式碼的空間複雜度主要看第一行即可,即 S(n) = O(n),同時時間複雜度也是O(n)。

間複雜度取決於額外建立的陣列m,如果使用二維陣列 new int[n][m] ,則空間複雜度是 O(n*m)

四、複雜度的選擇

對於相同的輸入規模,資料分佈不相同也影響了演算法執行路徑的不同,因此所需要的執行時間也不同。根據不同的輸入,將演算法的時間複雜度分析分為3種情況。
  • 1、最佳情況。使演算法執行時間最少的輸入。一般情況下,不進行演算法在最佳情況下的時間複雜度分析。如已經證明基於比較的排序演算法的時間複雜度下限為O(nlog2n),那麼就不需要白費力氣去想方設法將該類演算法改進為線性時間複雜度的演算法。

  • 2、最壞情況。使演算法執行時間最多的輸入。一般會進行演算法在最壞時間複雜度的分析,因為最壞情況是在任何輸入下執行時間的一個上限,它給我們提供一個保障,實際情況不會比這更糟糕。另外,對於某些演算法來說,最壞情況還是相當頻繁的。而且對於許多演算法來說,平均情況通常與最壞情況下的時間複雜度一樣。

  • 3、平均情況。演算法的平均執行時間,一般來說,這種情況很難分析。舉個簡單的例子,現要排序10個不同的整數,輸入就有10!種不同的情況,平均情況的時間複雜度要考慮每一種輸入及其該輸入的概率。平均情況分析可以按以下3個步驟進行:

    • 1 將所有的輸入按其執行時間分類
    • 2 確定每類輸入發生的概率
    • 3 確定每類輸入發生的概率

演算法很重要的一點就是時間換空間或者空間換時間

當追求一個較好的時間複雜度時,可能會使空間複雜度的效能變差,即可能導致佔用較多的儲存空間。

反之,求一個較好的空間複雜度時,可能會使時間複雜度的效能變差,即可能導致佔用較長的執行時間。

另外,演算法的所有效能之間都存在著或多或少的相互影響。因此,當設計一個演算法(特別是大型演算法)時,要綜合考慮演算法的各項效能,演算法的使用頻率,演算法處理的資料量的大小,演算法描述語言的特性,演算法執行的機器系統環境等各方面因素,才能夠設計出比較好的演算法。