1. 程式人生 > >演算法 時間和空間複雜度的簡單理解小述

演算法 時間和空間複雜度的簡單理解小述

一、概述

本節主要簡單分析下演算法的時間、空間複雜度,並不會涉及公式的推倒,主要以能用能理解為主,因為我自己也是一個門外漢,想深入的總結也是心有餘而力不足。

二、分析

當一個問題的演算法被確定以後,那麼接下來最重要的當然是評估一下該演算法使用的時間和佔用記憶體資源的相關問題了,如果其執行時間超出了我們能接受的底線,或者資源的佔用多到當前裝置不能滿足的程度,那麼對於我們來說,該演算法就是無用的,即使它能夠正確的執行。

相比於執行完程式再事後統計時間和佔用空間的方法,理論層面的複雜度分析更具有優勢,其主要表現在兩點,第一點:演算法執行所在的裝置,配置不同、執行環境的不同,都會給演算法本身執行的實際時間和空間的計算帶來偏差;第二點:測試資料規模的大小,資料本身的特殊性與否,也會造成在實際的執行結果不具有普適性,不容易正確反應演算法的效能的真實情況。

三、大O標記法

關於大O標記法的相關描述,我就直接引用「資料結構與演算法分析」書中的內容了,我覺得這裡講的非常透徹:一般來說,估計演算法資源消耗所需的分析是一個理論問題,因此需要一套正式的系統架構,我們先從某些數學定義開始。

定義:如果存在正常數 c 和 n0 使得當N ≥ n0 時,T(N) ≤ c f(N),則記為T(N) = O( f(N) )。

定義的目的是建立一種相對的級別。給定兩個函式,通常存在一些點,在這些點上一個函式的值小於另一個函式的值,因此,一般地宣稱,比如說f(N) < g(N) ,是沒有什麼意義的。於是,我們比較它們的相對增長率。當將相對增長率應用到演算法分析時,我們將會明白為什麼它是重要的度量。

雖然對於較小的 N 值,1000N 要比 N2 大,但N2 以更快的速度增長,因此 N2最終將是更大的函式。在這種情況下,N = 1000 是轉折點。定義是說,最後總會存在某個點 n0 ,從它以後 c · f(N) 總是至少與 T(N) 一樣大,從而若忽略常數因子,則 f(N) 至少與 T(N)一樣大。在我們的例子中,T(N) = 1000N,f(N) = N2,n0 = 1000 而 c=1。我們也可以讓 n0 = 10 而 c = 100。因此,可以說 1000N = O(N2)。這種記法稱之為大O標記法。人們常常不說“…級的”,而是說“大O…”。

好了,還有幾個定義我就不介紹了,跟當前定義的思路一樣,下面我就直接總結一下

函式表示式 含義
T(N) = O( f(N) ) 是說T(N) 的增長率小於或等於 f(N) 的增長率(符號讀音’大O’)
T(N) = Ω( g(N) ) 是說T(N) 的增長率大於或等於 g(N) 的增長率(符號讀音’omega’)
T(N) = Θ( h(N) ) 是說T(N) 的增長率等於 h(N) 的增長率(符號讀音’theta’)
T(N) = o( p(N) ) 是說T(N) 的增長率小於 p(N) 的增長率(符號讀音’小o’)

還有一點需要知道的是,當 T(N) = O( f(N) ) 時,我們是在保證函式 T(N) 是在以不快於 f(N)的速度增長,因此 f(N) 是T(N)的一個上界。這意味著 f(N) = Ω( T(N) ),於是我們說T(N)是f(N)的一個下界

四、時間複雜度分析

下面我們來看一段非常簡單的程式碼

1    private static int getNum(int n) {
2        int currentNum = 0;
3        for(int i=0; i< n; i++){
4            currentNum += i*i;
5        }
6        return currentNum;
7    }

在分析時,我們可以忽略呼叫方法、變數的宣告和返回值的開銷,所以我們只需要分析第2、3、4行的時間開銷:第2行佔用1個時間單元;第4行的1次執行實際佔用3個時間單元(1次乘法、1次加法、一次賦值),但是這麼精確的計算是沒有意義的,對於我們分析大O的結果也是無關緊要的,而且隨著程式的複雜度提高這種方式也會變得越來越不可操作,(推導過程就省略了,直接上結論了,本節主要是用法層面),所以我們也記第4行的1次執行時間開銷為1個時間單元,則 n 次執行開銷為 n 個時間單元;同理第3行執行 n 次的時間開銷也為 n 個時間單元,所以執行總開銷為 (2n + 1) 個時間單元。所以f(N) = 2n+1,根據上文T(N) = c · f(N)到T(N) = O(N2)的大O表示過程知道,我們可以拋棄一些前導的常數和拋棄低階項,所以T(N) = O(N)

知道了分析方法,下面我們再來看看其他複雜度的程式碼

1    private static void getNum(int n) {
2        int currentNum = 0;
3        for(int i=0; i< n; i++){
4            for(int j=0; j<n; j++){
5                currentNum++;
6            }
7        }
8    }

通過上面程式碼我們可知:第2行1個單元時間,第3行 n 個單元時間,第4行 n2 個單元時間,第5行 n2 個單元時間,所以總時間開銷f(N) = 2·n2 + n + 1,所以複雜度T(N) = O(N2),當然O(N3)都是同理的。

1    private static void getNum(int n) {
2        int currentNum = 0;
3        for(int k=0; k<n; k++){
4            currentNum++;
5        }
6        for(int i=0; i< n; i++){
7            for(int j=0; j<n; j++){
8                currentNum++;
9            }
         }
     }

通過上面程式碼我們可知:第2行1個單元時間,第3行 n 個單元時間,第4行 n 個單元時間,第6行 n 個單元時間,第7行 n2 個單元時間,第8行 n2 個單元時間,所以總時間開銷f(N) = 2·n2 +3·n + 1,所以複雜度T(N) = O(N2)

1        if(condition){
2            S1
3        }else{
4            S2
5        }

這是一段虛擬碼,在這裡主要是分析 if 語句的複雜度,在一個 if 語句中,它的執行時間從不超過判斷condition的執行時間加上 S1 和 S2 中執行時間長者的總的執行時間。

1    private static void getNum(int n) {
2        int currentNum = 0;
3        currentNum++;
4        if(currentNum > 0){
5            currentNum--;
6        }
7    }

通過上面的程式碼我們可知,第2行1個時間單元,第3行1個時間單元,第4行1個時間單元,第5行1個時間單元,所以總開銷4個時間單元,所以複雜度T(N) = O(1),注意這裡不是O(4)哦。

1    private static void getNum(int n, int m) {
2        int currentNum = 0;
3        for(int i=0; i<n; i++){
4            currentNum++;
5        }
6        for(int j=0; j<m; j++){
7            currentNum++;
8        }
9    }

通過上面的程式碼我們可知,第2行是1個單元時間,第3行是 n 個單元時間,第4行是 n 個單元時間,第6行是 m 個單元時間,第7行是 m 個時間單元,所以總的時間開銷f(N) = 2·n +2·m + 1,所以複雜度T(N) = O(n+m),同理,O(m·n)的複雜度也是同樣分析。

1    private static void getNum(int n) {
2        int currentNum = 1;
3        while (currentNum <= n) {
4            currentNum *= 2;
5        }
6    }

通過上面的程式碼我們可知,第2行需要1個單元時間;第3行每次執行需要1個單元時間,那麼現在需要執行多少次呢?通過分析我們知道當 2次數=n時while迴圈結束,所以次數 = log2n,所以第3行總需要 log2n 個單元時間;第4行同理也需要 log2n 個單元時間,所以總時間開銷f(N) = 2·log2n + 1,所以複雜度T(N) = O(logn),注意的是這裡不但省略了常數,係數,還省略了底哦。

1    private static void getNum(int n) {
2        int currentNum = 1;
3        for(int i=0; i<n; i++, currentNum=1){
4            while (currentNum <= n) {
5                currentNum *= 2;
6            }
7        }
8    }

通過上面的程式碼我們可知,第2行1個單元時間,第3行 n 個單元時間,第4行根據上文我們需要n·log2n個單元時間,第5行也需要n·log2n個單元時間,所以總時間花銷f(N) = 2·n·log2n + n + 1,所以複雜度T(N) = O(n·logn)

五、空間複雜度分析

上面我們簡單介紹了幾種常見的時間複雜度,空間的複雜度比時間複雜度要簡單許多,下面就來分析一下空間的複雜度:

空間複雜度考量的是演算法所需要的儲存空間問題,一般情況下,一個程式在機器上執行時,除了需要儲存程式本身的指令、常數、變數和輸入資料外,還需要儲存對資料操作的儲存單元,若輸入資料所佔空間只取決於問題本身,和演算法無關,這樣只需要分析該演算法在實現時所需的輔助單元即可。

1    private static void getNum(int n) {
2        int i = 0;
3        for(; i<n; i++){
4            i*=2;
5        }
6    }

通過上面的程式碼我們知道,第2行我們只需要1個空間單元;第3行、第4行不需要額外的輔助空間單元,所以空間複雜度S(N) = O(1),注意不是隻有1個空間單元才是O(1)哦,如果空間單元是常量階的複雜度都是O(1)哦。

1    private static void getNum(int n) {
2        int i = 0;
3        int[] array = new int[n];
4        for(; i<array.length; i++){
5            i*=2;
6        }
7    }

根據上面的程式碼我們可知,第2行需要1個空間單元;第3行需要 n 個空間單元;第4行、第5行不需要額外的空間單元,所以總消耗f(n) = n + 1,所以空間複雜度S(N) = O(n)哦,其他情況的分析與時間複雜度分析方法一樣,在這裡就不詳細介紹了。

六、總結

本節主要簡單介紹了複雜度的知識點,如果想要檢視更多演算法與資料結構知識,去我的部落格目錄裡檢視吧,因為關於每塊知識點的介紹,部落格單節寫的比較零散,不容易查詢。