1. 程式人生 > >演算法時間複雜度分析

演算法時間複雜度分析

演算法時間複雜度分析


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

時間複雜度直觀體現

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

斐波那契數列(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項的時候,第一種遞迴方式花費了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,只需要計算1次;第二層計算3和4,2次;計算第3層,4次;計算第4層,8次。所以總共計算1+2+4+8 =15= 25-1 = 1/2 * 22 -1

第一層計算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. 一定情況下,時間複雜度和空間複雜度可以互換。

關於複雜度的更多概念

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

總結

相關推薦

演算法演算法的概述和演算法時間複雜分析

演算法:演算法的概述和演算法時間複雜度分析 我是一名在校大學生,學習到了演算法這門課程,想整理一些筆記和大家分享,各位大佬不喜勿噴,僅供參考,希望能對大家有所幫助。 演算法,什麼是演算法 ? 它是求解問題的一系列計算步驟,用來將輸入的資料轉換成輸出結果。我總結關於演算法,有

遞迴演算法時間複雜分析

4.1  階乘n!的遞迴演算法的時間複雜度  時間複雜度是由語句頻度分析得來. 遞迴演算法中重複執行的語句主要是呼叫. 所以遞迴演算法的時間複雜度分析主要是分析遞迴演算法中遞迴函式呼叫的次數,並給出其呼叫次數的函式f(n). 如例1中,當n=5時fan(5)的呼叫情況如圖1所示:   從圖1中可以總結

演算法時間複雜分析(1)

如果有錯誤的地方,歡迎大家指正,只希望不要誤導別人。 開篇:      學習演算法時間複雜度分析,首先要對O、o、Ω、ω、Θ這幾個符號有基本的瞭解,下面將給出這幾個符號詳細的定義。     1、大O符號:         定義: 設f和g是定義域為自然數集N上的函式,若存

考研中的演算法時間複雜分析

1.常用的時間複雜度比較關係為O(1) <= O(log2(n)) <= O(n) <= O(nlog2(n)) <= O(n2) <= O(n3) ..... <=O(nk) <= O(2(n))2.具體步驟    1)確定演算法中

一、演算法時間複雜分析

    參考書目:《資料結構與演算法 (java語言班)》 P25    評價演算法的執行時間是通過分析在一定規模下演算法的基本操作完成的,並且我們只對大規模問題的執行時間感興趣。O、Ω、Θ分別定義了時間複雜度的上界、下界、精確階。    計算時間複雜度,最簡單的方式就是計算

演算法時間複雜分析

演算法時間複雜度分析 在看一個演算法是否優秀時,我們一般都要考慮一個演算法的時間複雜度和空間複雜度。現在隨著空間越來越大,時間複雜度成了一個演算法的重要指標,那麼如何估計一個演算法的時間複雜度呢? 時間複雜度直觀體現 首先看一個時間複雜度不同的兩個演算法,解決同一個問題,會有多大的區別。 下面兩個演算法都是

常見排序演算法的基本原理、程式碼實現和時間複雜分析

  排序演算法無論是在實際應用還是在工作面試中,都扮演著十分重要的角色。最近剛好在學習演算法導論,所以在這裡對常見的一些排序演算法的基本原理、程式碼實現和時間複雜度分析做一些總結 ,也算是對自己知識的鞏固。 說明: 1.本文所有的結果均按照非降序排列; 2.本文所有的程式均用c++實現,

資料結構與演算法--蠻力法之氣泡排序與時間複雜分析(java)

蠻力法         蠻力法又稱窮舉法和列舉法,是一種簡單直接地解決問題的方法,常常直接基於問題的描述,所以蠻力法也是最容易應用的方法。但是蠻力法所設計的演算法時間特性往往是比較低的,典型的指數時間演算法一般都是通過蠻力

KMP演算法介紹及時間複雜分析

概念:字串中 一個字元前面的字串 的字首與字尾的最長匹配長度(短的那個字串) 注意:字首與字尾不可以是整個子字串 例如:a b c a b c d , d位置的最長匹配長度為3,abc 與 abc 匹配 Next陣列:長度與字串長度一致,每個位置儲存對應字元的最長匹配長

遞迴演算法時間複雜分析

在演算法分析中,當一個演算法中包含遞迴呼叫時,其時間複雜度的分析會轉化為一個遞迴方程求解。實際上,這個問題是數學上求解漸近階的問題,而遞迴方程的形式多種多樣,其求解方法也是不一而足,比較常用的有以下四種方法:     (1)代入法(Substitution Method)  

排序演算法——希爾排序的圖解、程式碼實現以及時間複雜分析

希爾排序(Shellsort) 希爾排序是衝破二次時間屏障的第一批演算法之一。 希爾排序通過比較相距一定間隔的元素來工作;各躺比較所用的距離隨著演算法的進行而減小,直到只比較相鄰元素的最後一趟排序為止。由於這個原因,希爾排序有時也叫做縮減增量排序。 希爾排

常用排序演算法穩定性、時間複雜分析

1、  選擇排序、快速排序、希爾排序、堆排序不是穩定的排序演算法,        氣泡排序、插入排序、歸併排序和基數排序是穩定的排序演算法。 2、研究排序演算法的穩定性有何意義?   首先,排序演算法的穩定性大家應該都知道,通俗地講就是能保證排序前兩個相等的資

淺談直接插入排序演算法思想以及時間複雜分析

研究意義 直接插入排序是最基本的一種排序演算法,其思想簡單,容易掌握,對後期的學習也有一定的幫助。 必備知識(之後不再敘述) 排序:將一組雜亂無章的資料排列成一個按關鍵字有序的序列。 穩定性:關鍵值相

排序演算法 (穩定性時間複雜分析

1、  選擇排序、快速排序、希爾排序、堆排序不是穩定的排序演算法,        氣泡排序、插入排序、歸併排序和基數排序是穩定的排序演算法。 2、研究排序演算法的穩定性有何意義?   首先,排序演算法的穩定性大家應該都知道,通俗地講就是能保證排序前兩個相等的資

資料結構和演算法分析之排序篇--歸併排序(Merge Sort)和常用排序演算法時間複雜比較(附贈記憶方法)

歸併排序的基本思想 歸併排序法是將兩個或以上的有序表合併成一個新的有序表,即把待排序序列分成若干個子序列,每個子序列是有序的。然後再把有序子序列合併為整體有序序列。注意:一定要是有序序列! 歸併排序例項: 合併方法: 設r[i……n]由兩個有序子表r

JavaScript 演算法之最好、最壞時間複雜分析

上一篇文章中介紹了複雜度的分析,相信小夥伴們對常見程式碼的時間或者空間複雜度肯定能分析出來了。 思考測試 話不多說,出個題目考考大家,分析下面程式碼的時間複雜度(ps: 雖然說並不會這麼寫) function find(n, x, arr) { let ind = -1;

時間複雜分析:遞迴演算法

遞迴演算法中的時間複雜度有好幾種解法,在我看來,最容易掌握的是迭代法和遞迴樹法,這就列出這兩種 迭代法: *** 從原始遞推方程開始,反覆將對於遞推方程左邊的函式用右邊的等式代入,直到得到初值,然後將所得的結果進行化簡。 例如在呼叫歸併排序mergeSort(

快速排序演算法時間複雜分析(原地in-place分割槽版本)

快速排序演算法一般來說是採用遞迴來實現,其最關鍵的函式是partition分割函式,其功能是將陣列劃分為兩部分,一部分小於選定的pivot,另一部分大於選定的pivot。我們將重點放在該函式上面。 partition函式總體思路是自從一邊查詢,找到小於pivot的元素,則將

斐波那契數列遞迴演算法和非遞迴演算法以及其時間複雜分析

1、在學習資料結構這門課的過程中,發現斐波那契數列的遞迴演算法以及非遞迴演算法,以及其時間複雜度分析是一個小難點。所以特別總結一下。 斐波那契數列的表示式: Fibonacci數列簡介: F(1)=

歐幾里得演算法時間複雜簡單分析

前言 這個問題是在《資料結構與演算法C++描述(第三版中文)》所遇到的,文中給出的迭代次數O(logN)的結果就像是教授所說的“顯然”一樣高深莫測,有點雲裡霧裡的感覺!在“網羅”了一些資料後,在這裡找到了自己想要的答案,筆者接下來就結合自己的理解列出文章中的求