時間複雜度學習(上)

2018年10月9日java

1,定義

時間複雜度通常採用大O標記法, 即 T(n)=O(f(n)), 其中T(n)表示代碼運行時間;n表示數據規模大小;f(n)表示每行代碼執行次數總和,O 表示T(n)與f(n)的正比關係。大 O 時間複雜度實際上並不具體表示代碼的真正運行時間,而是表示代碼執行時間隨數據規模增加的變化趨勢。算法

在大 O 表示分析中,低階項常數項均可以省略,只保留最高階項便可;如 f(n)=2n+2在大 O 標記法中記爲 T(n)=O(n),而對於形如 f(n)=2n^2 +2n+3表示爲 T(n)=O(n^2)數組

2,時間複雜度分析

  • 關注循環次數多的代碼spa

    public int accumulate(int n) {
        int sum = 0;
        int i = 1;
        for (; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
    複製代碼

    其中for循環內的代碼執行n次,而其他代碼執行1次,與n的大小無關,忽略常數項,該段代碼的時間複雜度爲 O(n)code

  • 加法法則cdn

    總複雜度爲量級最大的那段代碼的複雜度,抽象爲公式爲:blog

    T_{1}(N)=O(f(n))T_{2}(N)=O(g(n)), 那麼 T(N)=T_{1}(N)+T_{2}(N)=O(f(n))+O(g(n))
    =max(O(f(n)),O(g(n)))排序

    public int accumulate(int n) {
       int sum1 = 0;
       for (int i = 1; i <= 100; i++) {
           sum1 += i;
       }
    
       int sum2 = 0;
       for (int i = 1; i <= n; i++) {
           sum2 += i;
       }
    
       int sum3 = 0;
       for (int i = 1; i <= n; i++) {
           for (int j = 1; j <= n; j++) {
               sum3 += i * j;
           }
       }
       return sum1 + sum2 + sum3;
    }
    複製代碼

    其中sum1段的代碼循環執行了100次,與n無關。sum2段代碼的複雜度爲 O(N),sum3段的代碼複雜度爲 O(N^2);根據加法法則,咱們只去其中最大量級的複雜度,因此該段代碼的時間複雜度爲 O(N^2)it

  • 乘法法則io

    嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積,抽象爲公式爲:

    T_{1}(N)=O(f(n))T_{2}(N)=O(g(n)),那麼T(N)=T_{1}(N)*T_{2}(N)=O(f(n)*g(n))

    public int accumulate(int n) {
        int result = 0;
        for (int i = 1; i <= n; i++) {
            result += f(i);
        }
        return result;
    }
    
    private int f(int n) {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
    複製代碼

    其中accumulate方法與f方法的時間複雜度都爲 O(N),可是f嵌套在accumulate中,因此整段代碼的複雜度就爲:T(N)=T_{1}(N)*T_{2}(N)=O(N * N)=O(N^2)

3,常見時間複雜度量級

度量級 O 表示
常量階 O(1)
對數階 O(logN)
線性階 O(N)
線性對數階 O(NlogN)
平方階 O(N^2)
立方階 O(N^3)
指數階 O(2^n)
階乘階 O(N!)

  • 常見的時間複雜度有常量階、對數階、線性階、線性對數階以及平方階,常量階、線性階與平方階在第二節中已經分析,再也不贅述;而一些高效的排序算法的時間複雜度就是線性對數階,如快速排序,歸併排序以及堆排序等。

  • 對數階

    咱們所熟知的二分查找的複雜度就是 O(logN),如下經過一段代碼來分析對數階複雜度:

    public int test(int n) {
        int res = 1;
        while (res <= n) {
            res *= 2;
        }
        return res;
    }
    複製代碼

    該段代碼是求 2^x=n的解,更確切的說,是找出 2^x在小於或等於n的範圍內最接近n的x的值;其中 x=\log_{2}n,即while循環體內代碼要執行 \log_{2}n次,即其時間複雜度爲 O(\log_{2}n)

    若把循環體內代碼 res *= 2 改成 res *= 3 ,不難分析出其時間複雜度就變爲 O(\log_{3}n);可是爲何全部對數階的時間複雜度都統一表示爲 O(logN)

    首先咱們先複習對數換底公式:

    \log_{A}B=\frac{\log_{C}B}{\log_{C}A}

    \log_{3}n=\frac{\log_{2}n}{\log_{2}3}=\frac{\log_{2}2}{\log_{2}3}\cdot\log_{2}n=\log_{3}2\cdot\log_{2}n

    因此 O(\log_{3}n)=O(\log_{3}2\cdot\log_{2}n),由於\log_{3}2爲常數項,因此該項能夠忽略,所以O(\log_{3}n)=O(\log_{2}n);因此不管對數以哪一個數爲底,最後均可以轉化爲一個常數項與以2爲底的對數相乘,所以在對數階時間複雜度的表示方法裏,就忽略對數的底,統一表示爲 O(log N)

  • O(m+n)與O(m*n)

    此種表示形式的時間複雜度是由兩個數據規模來決定的

    public int accumulate(int m, int n) {
        int sum1 = 0;
        for (int i = 1; i <= m; i++) {
            sum1 += i;
        }
    
        int sum2 = 0;
        for (int i = 1; i <= n; i++) {
            sum2 += i;
        }
    
        return sum1 + sum2;
    }
    複製代碼

    因爲咱們不能事先知曉m與n哪一個量級大,因此就不能簡單的利用加法規則取其最大量級,那麼像這種代碼的時間複雜度就爲 O(m+n)

    public int accumulate(int m, int n) {
        int sum = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum += i * j;
            }
        }
        return sum;
    }
    複製代碼

    而相似上述代碼依然可使用乘法法則,其時間複雜度爲 O(m*n)

4,最好、最壞、平均和均攤時間複雜度

如下將經過一段代碼來說述這幾個時間複雜度:

public class Test {
    private int[] array = new int[5];
    private int N = 0;

    public void push(int item) {
        if (N == array.length) {
            resize(2 * array.length);
        }
        array[N++] = item;
    }

    private void resize(int size) {
        int[] temp = new int[size];
        for (int i = 0; i < N; i++) {
            temp[i] = array[i];
        }
        array = temp;
    }
}
複製代碼

上述代碼是用數組模擬一個棧的部分代碼,其中push表示壓棧操做,resize表示對數組進行擴容的操做;當壓入棧中的元素數量達到數組的容量時,就定義一個容量爲以前兩倍的新數組temp,將舊數組array中的元素複製到新數組中,而後將array指向temp

  • 最好時間複雜度:最理想的狀況下,當前棧中元素數量比數組的容量小,此時就直接執行代碼塊array[N++] = item;,即此時的時間複雜度爲 O(1)

  • 最壞時間複雜度:最糟糕的狀況下,當前棧中元素數量與數組的容量相等,此時就要執行resize方法進行擴容了,進入循環體,執行N次複製操做,此時的時間複雜度爲 O(N)

  • 平均時間複雜度

    • 當棧中元素小於數組容量時,此時進行壓棧就有N中狀況,且每種狀況的時間複雜度爲 O(1);當棧中元素與數組容量相等時,此時進行壓棧就只有一種狀況了,要進行擴容操做,這種狀況的時間複雜度爲 O(N);則總共有N+1中狀況,對其取平均值:
    \cfrac{1+1+1+...+1+N}{N+1}=\cfrac{2N}{N+1}

    在大 O標記法中,能夠省略係數與低階項,因此其平均時間複雜度爲 O(1)

    • 下面使用機率來分析,因爲有N+1中狀況,每種狀況的發生機率爲 \frac{1}{N+1},則其平均時間複雜度爲:
    1\times\frac{1}{N+1}+1\times\frac{1}{N+1}+...+1\times\frac{1}{N+1}+N\times\frac{1}{N+1}=O(1)
  • 均攤時間複雜度:是一種特殊的平均時間複雜度,根據上述代碼,每出現一次擴容操做時,即此時壓棧的時間複雜度爲 O(N),那麼後面的N次壓棧操做的時間複雜度均爲 O(1),先後是連貫的,所以將 O(N)平攤到前N次上,得出均攤時間複雜度爲 O(1)

相關文章
相關標籤/搜索