算法時間復雜度分析


算法時間復雜度分析


在看一個算法是否優秀時,我們一般都要考慮一個算法的時間復雜度和空間復雜度。現在隨着空間越來越大,時間復雜度成了一個算法的重要指標,那么如何估計一個算法的時間復雜度呢?

時間復雜度直觀體現

首先看一個時間復雜度不同的兩個算法,解決同一個問題,會有多大的區別。
下面兩個算法都是用來計算斐波那契數列的,兩個算法會有多大的差異。

斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……在數學上,斐波那契數列以如下被以遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)

  • 第一種:使用遞歸方式
    /**
     * 使用遞歸方式計算斐波拉契數列
     * @param  index 計算的項數
     */
    public static long fibonacciUseRecursion(int index){
        if(index <= 1){
            return index;
        }
        return fibonacciUseRecursion(index-1) + fibonacciUseRecursion(index-2);
    }
  • 第二種:使用非遞歸方式
    /**
     * 不使用遞歸方式計算斐波拉契數列
     * @param index 計算的項數
     */
    public static long fibonacciNoUseRecursion(int index){
        if (index <= 1){
            return index;
        }
        long first = 0;
        long second = 1;
        for (int i = 0; i < index - 1;i++){
            second = first + second;
            first = second - first;
        }
        return second;
    }

對上面兩種算法進行簡單的運行時間統計,我們使用下面的代碼進行簡單的測試

public static void main(String[] args) {
        // 獲取當前時間
        long begin = System.currentTimeMillis();
        // 計算第50項斐波拉契數列的值
        System.out.println(fibonacciUseRecursion(50));
        // 計算時間差,算法執行所花的時間
        System.out.println("time:" + (System.currentTimeMillis() - begin) / 1000 +"s");
        
        begin = System.currentTimeMillis();
        System.out.println(fibonacciNoUseRecursion(50));
        System.out.println("time:" + (System.currentTimeMillis() - begin) / 1000 + "s");
    }

測試結果如下:

計算50項結果.png

計算第51項結果.png

可以看到,在計算第50項的時候,第一種遞歸方式花費了48秒的時間,而第二種不到一秒,雖然這種方式不太科學,但也看出來了兩者巨大的差距,並且隨着計算的值越大,時間的差異越明顯。由此可見,時間復雜度是決定一個算法好壞的重要指標。

如何衡量一個算法的好壞

  1. 正確性、可讀性、健壯性。
    算法必須要保證正確,不正確的算法是沒有必要衡量其好壞的;算法也要保證良好的可讀性,能夠讓閱讀者明白內在實現與邏輯;健壯性為對不合理輸入的反應能力和處理能力,比如非法輸入,要有相應的處理,而不應該程序奔潰等。這些都是一個良好的算法必備的條件。
  2. 時間復雜度
    時間復雜度也是一個衡量算法優劣的重要條件,不同的算法的執行時間可能會存在很大的差別。
  3. 空間復雜度
    空間復雜度表示一個算法執行過程中,需要的空間(內存)數量,也是衡量一個算法的重要指標,尤其是在嵌入式等程序中的算法,內存是非常寶貴的,有時候寧願提高時間復雜度,也要保證不占用太多的空間。

如何計算時間復雜度

第一種:事后統計法

上面我們使用了一種計算執行前后時間差的方式,直觀的來看一個算法的復雜度,比較不同算法對同一組輸入的執行時間,這種方法也叫作"事后統計法",但是這種方法也存在一些問題,主要問題有:

  • 執行時間嚴重依賴於硬件已經運行時各種不確定的環境因素。
    比如兩個算法在不同的硬件機器上進行測試,硬件不同,運行時間也會存在差異,即使就在一台機器上執行,也會存在運行時機器的CPU、內存使用情況不同等因素。
  • 必須要編寫相應的測試代碼。
  • 測試數據的選擇難以保證公正性。
    比如有兩個算法,一個在數據量小的時候占優,一個在大數據量的時候運行較快,這樣便難以選擇一個公正的測試數據。

第二種:估算代碼指令執行次數

那么我們可以使用代碼的每個指令的執行次數,可以簡單估算代碼的執行次數,一般情況下,執行次數少的肯定要比執行次數多的花的時間更少。看如下的示例:

    public static void test1(int n) {
        if (n > 10) {
            System.out.println("n > 10");
        } else if (n > 5) { 
            System.out.println("n > 5");
        } else {
            System.out.println("n <= 5");
        }
        
        for (int i = 0; i < 4; i++) {
            System.out.println("test");
        }
    }

上面這個方法,我們計算它的執行次數。

  1. 最上面的if...else if...else這個判斷,判斷會執行一次、判斷成立的代碼會執行一次。
  2. 下面的for循環,i=0這句賦值會執行一次,i<4這個判斷條件會執行4次,i++也會執行4次,循環體(輸出語句)也會執行4次。
  3. 因此,整個方法的執行次數為:1+1+1+4+4+4 = 15次。
    public static void test2(int n) {
        for (int i = 0; i < n; i++) {
            System.out.println("test");
        }
    }

上面這個方法,我們計算它的執行次數。

  1. 在for循環中,i=0這句賦值會執行一次,i < n執行n次,i++執行n次,循環體執行n次。
  2. 因此,整個方法的執行次數為:1+n+n+n = 3n+1 次
    public static void test3(int n) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                System.out.println("test");
            }
        }
    }

上面這個方法,我們計算它的執行次數。

  1. 在外層for循環中,i=0這句賦值會執行一次,i < n執行n次,i++執行n次,循環體執行n次。
  2. 在內層循環中,j=0這句賦值會執行一次,j < n執行n次,j++執行n次,循環體執行n次。
  3. 因此,整個方法的執行次數為 1+n+n+n*(1+n+n+n)=3n2+3n+1 次
    public static void test4(int n) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < 15; j++) {
                System.out.println("test");
            }
        }
    }

上面這個方法,我們計算它的執行次數。

  1. 在外層for循環中,i=0這句賦值會執行一次,i < n執行n次,i++執行n次,循環體執行n次。
  2. 在內層循環中,j=0這句賦值會執行一次,j < 15執行15次,j++執行15次,循環體執行15次。
  3. 因此,整個方法的執行次數為 1+n+n+n*(1+15+15+15)=48n+1 次
    public static void test5(int n) {
        while ((n = n / 2) > 0) {
            System.out.println("test");
        }
    }

上面這個方法,我們計算它的執行次數。

  1. 在while循環中,每次對n取一半,相當於對n取以二為底的對數,因此n = n / 2 會執行log2(n)次,判斷條件也會執行log2(n)次。
  2. 在循環體中,這個輸出語句也會執行log2(n)次。
  3. 因此,整個方法的執行次數為 log2(n) + log2(n) + log2(n) = 3log2(n)次
    public static void test6(int n) {
        while ((n = n / 5) > 0) {
            System.out.println("test");
        }
    }

上面這個方法,我們計算它的執行次數。

  1. 在while循環中,每次對n取五分之一,相當於對n取以五為底的對數,因此n = n / 5 會執行log5(n)次,判斷條件也會執行log5(n)次。
  2. 在循環體中,這個輸出語句也會執行log5(n)次。
  3. 因此,整個方法的執行次數為 log5(n) + log5(n) + log5(n) = 3log5(n)次
    public static void test7(int n) {
        for (int i = 1; i < n; i = i * 2) {
            for (int j = 0; j < n; j++) {
                System.out.println("test");
            }
        }
    }

上面這個方法,我們計算它的執行次數。

  1. 在外層for循環中,i= 1執行一遍,每次i翻倍,執行次數為log2(n),因此i < n會執行log2(n)次,i=i*2會執行log2(n)次,循環體執行log2(n)。
  2. 在內層for循環中,j=0執行一次,j < n執行n次,j++執行n次,內層循環條件執行n次。
  3. 因此,整個方法的執行次數為 1+ log2(n) + log2(n) + log2(n)*(1+n+n+n) = 3nlog2(n) + 3log2(n)+1次
    public static void test8(int n) {
        int a = 10;
        int b = 20;
        int c = a + b;
        int[] array = new int[n];
        for (int i = 0; i < array.length; i++) {
            System.out.println(array[i] + c);
        }
    }

上面這個方法,我們計算它的執行次數。

  1. a=10執行一次,b=20執行一次,c=a+b執行一次,初始化數組執行一次。
  2. 在for循環中,i=0執行一次,i < 數組長度執行n次,i++執行n次,內層循環條件執行n次。
  3. 因此,整個方法的執行次數為 1+1+1+1+1+n+n+n =3n +5次。

使用這種方法我們發現計算會特別麻煩,而且不同的時間復雜度表達書也比較復雜,我們也不好比較兩個時間復雜度的具體優劣,因此為了更簡單、更好的比較不同算法的時間復雜度優劣,提出了一種新的時間
復雜度表示法---大O表示法。

大O表示法

大O表示法:算法的時間復雜度通常用大O符號表述,定義為T[n] = O(f(n))。稱函數T(n)以f(n)為界或者稱T(n)受限於f(n)。 如果一個問題的規模是n,解這一問題的某一算法所需要的時間為T(n)。T(n)稱為這一算法的“時間復雜度”。當輸入量n逐漸加大時,時間復雜度的極限情形稱為算法的“漸近時間復雜度”。

大O表示法,用來描述復雜度,它表示的是數據規模n對應的復雜度,大O表示法有以下的一些特性:

  1. 忽略表達式常數、系數、低階項。
    忽略常數,常數直接為1,比如上面第一個方法的復雜度為15,因此直接取1,其時間復雜度使用大O表示為O(1)。
    忽略系數,忽略表達式的系數,比如第二個方法的時間復雜度為3n+1,忽略系數和常數,其時間復雜度為O(n)。
    忽略低階項,比如第三個方法的時間復雜度為3n2+3n+1,忽略低階項3n,忽略常數1,忽略系數3,則其時間復雜度為O(n2)。
  2. 對數階一般忽略底數
    對於對數直接的轉換,一個對數都可以乘以一個常數項成為一個沒有底數的對數,比如
    log2n = log29 * log9n,因此可以省略底數,比如上面第五個方法的時間復雜度為log2(n),可以忽略底數2,則其時間負責度為logn。
  3. 大O表示法僅僅是一種粗略的分析模型,是一種估算,能幫助我們短時間內估算一個算法的時間復雜度。

常見的復雜度

執行次數 復雜度 非正式術語
12 O(1) 常數階
2n+3 O(n) 線性階
4n2+zn+2 O(n2) 平方階
4log2n+21 O(logn) 對數階
3n+2log3n+15 O(nlogn) nlogn階
4n3+3n2+22n+11 O(n3) 立方階
2n O(2n) 指數階

復雜度的大小關系
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)。

因此上面的十個方法的復雜度如下:

方法名稱 復雜度 大O表式
test1 15 O(1)
test2 3n+1 O(n)
test3 3n2+3n+1 O(n2)
test4 48n+1 O(n)
test5 3log2(n) O(logn)
test6 3log5(n) O(logn)
test7 3nlog2(n) + 3log2(n) + 1 O(nlogn)
test8 3n+5 O(n)

直觀對比復雜的的大小

直接看表達式,還是很難判斷一些復雜度的大小關系,我們可以借助可視化的一些工具來查看比如https://zh.numberempire.com/graphingcalculator.php,通過該網站我們看到在n變化的情況下,不同表達式的變換情況。

遞歸斐波拉契數列計算方法的時間復雜度分析

計算第5項.png

第一層計算5,只需要計算1次;第二層計算3和4,2次;計算第3層,4次;計算第4層,8次。所以總共計算1+2+4+8 =15= 25-1 = 1/2 * 22 -1

計算第6項.png

第一層計算6,只需要計算1次;第二層計算5和4,2次;計算第3層,4次;計算第4層,8次;第5層,計算10次。所以總共計算1+2+4+8+10 =25 = 25 - 7 = 1/2 * 26 - 7。
所以計算第n項,它的時間復雜度為O(2^n)。
所以最開始的兩個算法,第一個的算法復雜度為O(2n),一個為O(n)。
他們的差別有多大?

  1. 如果有一台1GHz的普通計算機,運算速度109次每秒(n為64)
  2. O(n)大約耗時6.4 ∗ 10-8
  3. O(2n)大約耗時584.94年
  4. 有時候算法之間的差距,往往比硬件方面的差距還要大

算法的優化方向

  1. 用盡量少的存儲空間,即空間復雜度低。
  2. 用盡量少的執行步驟,即時間復雜度低。
  3. 一定情況下,時間復雜度和空間復雜度可以互換。

關於復雜度的更多概念

  • 最好、最壞復雜度
  • 均攤復雜度
  • 復雜度震盪
  • 平均復雜度
  • ......

總結

祝你好運


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM