復雜度分析


  同一個問題可以使用不同的算法解決,那么不同的算法孰優孰劣如何區分呢?因此我們需要一個表示方法來代表每個程序的效率。

 

  衡量一個程序好壞的標准,一般是運行時間與占用內存兩個指標。

  不過我們在寫代碼的時候肯定無法去估量程序的執行時間,因為真實的執行時間受到多方面因素的影響,比如同樣一段程序,放在高配服務器上跑和放在低配服務器上跑完全是兩個表現效果,比如遍歷一個數組的函數,執行時間完全取決於調用函數傳入的數組大小。

 

  如何在不運行程序的情況下,判斷出代碼的執行時間呢?顯然是不可能的。

 

  不過我們雖然無法預估代碼的絕對執行時間,但是我們可以預估代碼基本的執行次數。

  一段代碼的執行時間如果有變化,則一定是受到外部輸入的數據所影響,我們將代碼中所有不變的因素,表示為大O,將變化的因素作為基數n,表示為:O(n),大O的意思是忽略重要項以外的內容,我們常以這種大O表示法來判斷比較各種算法的執行效率。

 

  接下來我會介紹幾種常用的復雜度表示方法。

 

  PS:專業的解釋必然全篇都是數學證明,未免太過於復雜,讓學者更加迷茫,我這里寫的並不是教材,而是最直白的理解。

 

時間復雜度

  本節中例舉的各種時間復雜度以好到差依次排序。

常數時間 O(1)

  先看下這個函數:

    private static void test(int n)
    {
        int a = 2;
        int b = 3;
        int c = a+b;
        System.out.println(c);
    }

  一共4行代碼,CPU要將a的值寫入內存,b的值寫入內存,a和b進行計算,將計算結果寫入c,最后將c輸出到控制台。

  盡管計算機內部要做這么多事情,這段代碼的時間復雜度依然是O(1),原因是這幾行代碼所做的操作是固定的,是不變的因素。

 

  再看下這個函數:

    private static void test(int n)
    {
        for (int i=0;i<100000;i++){
            System.out.println(i);
        }
    }

  循環10W次,可能你覺得功耗可能有點大,不過它的時間復雜度仍然是O(1)。

 

  我們可以這么固定的認為:無論接收的參數怎么變化,只要代碼執行次數是無變化的,則用1來表示。 凡是O(1)復雜度的代碼,通常代表着它是效率最優的方案。

 

對數時間 O(log n)

  普遍性的說法是復雜度減半,就像紙張對折。

  示例代碼:

    private static void test(int n)
    {
        for (int j=1;j<=n;j=j*2){
            System.out.println(j);
        }
    }

  這段代碼的執行效果並非是一次折半,它是次次折半,以2為底,不斷的進行冪運算,實際上只要有冪指數關系的,不管你的底數是幾,只要能夠對原復雜度進行求冪逆運算我們都可以稱之為O(log n)

  比如:

    private static void test(int n)
    {
        for (int j=1;j<=n;j=j*3){
            System.out.println(j);
        }
    }

  在忽略系數、常數、底數之后,最后都可以表示為O(log n),只不過我們遇到的算法幾乎不會出現一些極端例外情況,對數時間的所在地常見以二分查找為代表。

 

線性時間 O(n)

  我們將test方法稍稍修改一下:

    private static void test(int n)
    {
        for (int i=0;i<n;i++){
            System.out.println(i);
        }
    }

  修改之后這次不是執行10W次,而是執行n次,n是由參數傳入的一個未知值,在沒有真實運行的時候我們無法判斷這個n到底是多少?因為它可以是任意int型數字,你可以這么認為:在理想的情況下,它的復雜度是O(1),在惡劣的情況下,它的復雜度是無限大。完全取決於方法調用方。

  直白的說,for循環就是循環n次,因此這段代碼的時間復雜度為O(n),這種復雜度常常表現為線性查找。

 

線性對數時間 O(n log n)

  線性對數時間也就是線性時間嵌套對數時間:

    private static void t(int n){
        for (int i=0;i<n;i++){
            test(n);
        }
    }
    private static void test(int n)
    {
        for (int j=1;j<=n;j=j*2){
            System.out.println(j);
        }
    }

  t這個方法的時間復雜度就是O(n log n)

 

平方時間 O(n^2)

  平方時間就是執行程序需要的步驟數是輸入參數的平方,最常見的是嵌套循環:

    private static void test(int n)
    {
        for (int i=0;i<n;i++){
            for (int j=n;j>0;j--){
                System.out.println(j);
            }
        }
    }

 

其他時間

  比O(n^2)還要慢的自然有立方級O(n^3)

  比O(n^3)更慢慢的還有指數級O(2^n)

  慢到運行一次程序要繞地球三百圈的有O(n!)

 

  正常情況下我們不會接觸到這些類型的算法。

 

空間復雜度

  所謂空間,就是程序運行占用的內存空間,空間復雜度指的就是執行算法的空間成本。

 

  這里我們拋一道題來做例子:在一個數組中找出有重復的值,如數組[3,8,13,7,15,8,6,6] 找出8和6

 

  解法:

    private static void test(int[] arr)
    {
        for (int i=0;i<arr.length;i++){
            for(int j=0;j<i;j++){
                if(arr[j] == arr[i]){
                    System.out.println("找到了:"+arr[i]);
                }
            }
        }
    }

  很顯然:時間復雜度為O(n^2)。

 

  那我們還可以使用一種更優的解法:

    private static void test(int[] arr)
    {
        HashSet hashSet = new HashSet();
        for (int i=0;i<arr.length;i++){
            if(hashSet.contains(arr[i])){
                System.out.println("找到了:"+arr[i]);
            }
            hashSet.add(arr[i]);
        }
    }

  也許你會驚訝的發現,時間復雜度被優化成了O(n)。

 

  雖然時間復雜度降低成了O(n),但是付出的代價是空間復雜度變成了O(n),因為新的解法使用了一個HashSet來存儲數據,存儲數據自然要占用內存空間,而占用的空間大小完全取決於傳入數組大小。

  我們之所以說第二種解法更優,其實是一種常規思想,因為現實中絕大部分情況,時間復雜度顯然比空間復雜度更為重要,我們寧願多分配一些存儲空間作為代價,來提升程序的執行速度。

 

  總而言之,比較兩個算法優劣的指標有兩個,時間復雜度與空間復雜度,優先比較時間復雜度,時間復雜度相同的情況下比較空間復雜度。

 

  最后:感謝閱讀。


免責聲明!

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



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