搞編程,你必知必會的複雜度分析

在開發中,咱們會常常聽到關於時間複雜度、空間複雜度相關詞彙,若是你沒有這方面的知識,你確定會一臉懵逼。那什麼是時間複雜度、空間複雜度還有咱們又怎麼去分析?首先咱們先來弄清楚咱們爲何須要作複雜度分析。java

爲何須要複雜度分析?

真實的時間複雜度、空間複雜度咱們須要在機器上執行咱們編寫的代碼,才能統計出咱們的代碼這這個環境下的真實時間複雜度、空間複雜度。這種方法統計出來的結果很是準確,可是極限性也很是大。算法

1. 測試結果很是依賴測試環境

測試環境中硬件的不一樣會對測試結果有很大的影響。好比,拿一樣一段代碼,分別用 Intel Core i9 處理器和 IntelCore i3處理器來運行,不用說,i9處理器要比 i3 處理器執行的速度快不少。還有,好比本來在這臺機器上 a 代碼執行的速度比 b 代碼要快,當換到另外一臺機器上時,可能 會有截然相反的結果。數組

2. 測試結果受數據規模的影響很大

好比排序算法,對同一個排序算法,待排序數據的有序度不同,排序的執行時間就會有很大的差異。極端狀況下,若是數據已是有序的,那排序算法不須要作任何操做,執行時間就會很是短。除此以外,若是測試數據規模過小,測試結果可能沒法真實地反應算法的性能。好比,對於小規模的數據排序,插入排序可能反倒會比快速排序要快!微信

那能不能不用具體的測試數據來測試,就能夠粗略地估計算法的執行效率的方法?答案是確定的,也就是咱們的主題時間複雜度、空間複雜度的分析,通常用大O公式來進行代碼時間複雜度、空間複雜度的預測分析。數據結構

大 O 複雜度表示法

1   public void sum(int n) {
2      int sum = 0;
3       for (int i = 1; i <= n; i++) {
4           sum += i;
5       }
6       System.out.println(sum);
7   }
複製代碼

假設每行代碼的執行時間爲time,咱們來粗略估計一下這段代碼塊的執行總時間,第二行代碼執行須要1個time,第三、4行代碼都執行了n遍,因此須要的時間爲n * time,第6行代碼執行的時間爲1個time,因此整個代碼塊的執行時間爲(2n+2) * time,若是咱們用 T(n) 函數來表示代碼的執行總時間,那麼T(n) = (2n+2) * time能夠看出 T(n) 與 n 成正比關係。這就能夠用大O公式來表示。函數

大O公式:T(n)=O(f(n))性能

T(n) 表示代碼執行的時間;n 表示數據規 模的大小;f(n) 表示每行代碼執行的次數總和。 O 表示代碼的執行時間 T(n) 與 f(n) 表達式成正比。這就是大 O 時間複雜度表示法。大 O時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨 數據規模增加的變化趨勢,因此,也叫做漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。 當 n 很大時,你能夠把它想象成 10000、100000。而公式中的低階、常量、係數三部分並不左右增 長趨勢,因此均可以忽略。咱們只須要記錄一個最大量級就能夠了,因此咱們示例中的總時間就能夠用 T(n) =O(n) 來標識。學習

時間複雜度分析

前面咱們已經瞭解了大O公式,那咱們如何進行代碼的複雜度分析呢?能夠從如下三個準則入手。測試

一、只關注循環執行次數最多的一段代碼

咱們知道用大O公式來表示時間複雜度的時候,忽略了常量、低階、係數等,咱們只須要關注循環執行次數最多的那一段代碼就能夠了,這段代碼執行的次數 n 就是整個代碼塊的時間複雜度。爲了方便咱們理解這段話,咱們用上面的代碼來分析一下,增強理解。spa

1   public void sum(int n) {
2      int sum = 0;
3       for (int i = 1; i <= n; i++) {
4           sum += i;
5       }
6       System.out.println(sum);
7   }
複製代碼

代碼 二、6 都是常量級的執行時間,對時間複雜度沒有影響,執行最多的代碼是 三、4 兩行代碼,一共執行了 n 次,因此整個代碼塊的時間複雜度爲 O(n)

二、總複雜度等於量級最大的那段代碼的複雜度

1    public void test(int n) {
2        for (int i = 0; i < n; i++) {
3            System.out.println(i);
4        }
5        for (int i = 0; i < n; i++) {
6            for (int j = 0; j < n; j++) {
7                System.out.println(i * j);
8            }
9        }
10    }
複製代碼

這段代碼有兩個時間複雜度,2-4 行代碼的時間複雜度 T1(n) = O(n),5-8 行代碼的時間複雜度爲 T2(n) = O(n²)。當 n 無限大的時候,T1(n) 對整個代碼塊的時間複雜度的影響是能夠忽略的,整個代碼塊的時間複雜度就爲 T2(n)=O(n²),換句話說總的時間複雜度就等於量級最大的那段代碼的時間複雜度。那咱們將這個規律抽象成公式就是: 若是 T1(n)=O(f(n)),T2(n)=O(g(n));那麼 T(n)=T1(n)+T2(n)=max(O(f(n)),O(g(n)))=O(max(f(n),g(n)))

三、嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積

1    public void test(int n) {
2        for (int i = 0; i < n; i++) {
3            for (int j = 0; j < n; j++) {
4                System.out.println(i * j);
5            }
6       }
7    }
複製代碼

這段代碼中第二行代碼的複雜度爲T1(n)=O(n),第3行代碼塊的T2(n)=O(n),第四行代碼的複雜度爲O(1)能夠忽略,由於這段代碼是循環,因此時間複雜度T(n) = T1(n) * T2(n) = O(n * n) = O(n²)

經過上面的三種準則就可以很好的分析代碼的時間複雜度,雖然代碼千奇百怪,可是常見的複雜度量級並很少,咱們來看看幾種常見時間複雜度。

  • 常數階O(1)
  • 對數階O(logN)
  • 線性階O(n)
  • 線性對數階O(nlogN)
  • 平方階O(n²)
  • 立方階O(n³)
  • K次方階O(n^k)
  • 指數階(2^n)

上面從上至下依次的時間複雜度愈來愈大,執行的效率愈來愈低,咱們來看看幾種常見覆雜的案例。

常數階O(1)

常數階很是簡單,就是沒有變量,都是常量,那樣代碼的時間複雜度就爲 O(1)。下面兩段代碼的時間複雜度都爲 O(1)。

public void test(){
    for (int i = 0;i <100;i++){
        System.out.println(i);
    }
}

public void sum(int n) {
    int i = 2;
    int j = 6;
    int sum = i + j;
    System.out.println(sum);
}
複製代碼

對數階O(logN)

i=1; 
 while (i <= n) { 
     i = i * 2;
 }
複製代碼

從上面代碼能夠看到,在while循環裏面,每次都將 i 乘以 2,乘完以後,i 距離 n 就愈來愈近了。咱們試着求解一下,假設循環x次以後,i 就大於 2 了,此時這個循環就退出了,也就是說 2 的 x 次方等於 n,那麼 x = log2^n 也就是說當循環 log2^n 次之後,這個代碼就結束了。所以這個代碼的時間複雜度爲:O(logn)

線性階O(n)

public void sum(int n) {
    int sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    System.out.println(sum);
}
複製代碼

這段代碼,for循環裏面的代碼會執行n遍,所以它消耗的時間是隨着n的變化而變化的,所以這類單層循環的代碼均可以用O(n)來表示它的時間複雜度。

線性對數階O(nlogN)

public void test1(int n) {
    for (int i = 0; i < n; i++) {
        int m = 0;
        while (m < n) {
            m *= 2;
        }
    }
}
複製代碼

線性對數階O(nlogN) 其實很是容易理解,將時間複雜度爲O(logn)的代碼循環N遍的話,那麼它的時間複雜度就是 n * O(logN),也就是了 O(nlogN)。

平方階O(n²)

public void test(int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            System.out.println(i * j);
        }
    }
}
複製代碼

平方階 O(n²) 就是兩層循環,每層循環的次數是一個變量,這種的兩層循環的代碼的時間複雜度均可以用 O(n²) 表示。立方階O(n³)、K次方階O(n^k)跟這個同樣,只是多層循環而已。

上面就是經常使用時間複雜度的案例,在時間複雜度分析中,你也許還據說過最好狀況時間複雜度最壞狀況時間複雜度平均狀況時間複雜,那這些又是什麼呢?先來看一段案例。

public int find(int[] array, int n, int x) {
    int i = 0;
    int pos = -1;
    for (; i < n; ++i) {
        if (array[i] == x) {
            pos = i;
            break;
        }
    }
    return pos;
}
複製代碼

上面的代碼是在數組中找出值等於x的下標。根據上面學習的大O公式,這段代碼的時間複雜度爲 O(n),可是這段代碼的時間複雜度必定爲O(n)嗎?不必定的,若是數組中第一個元素正好是要查找的變量x,那就不須要繼續遍歷剩下的 n-1 個數據了,那時間複雜度就是O(1)。但若是數組中不存在變量x,那就須要把整個數組都遍歷一遍,時間複雜度就成了O(n)。因此,不一樣的狀況下,這段代碼的時間複雜度是不同的。

爲了表示代碼在不一樣狀況下的不一樣時間複雜度,就須要引入上面提到的三個概念:最好狀況時間複雜度、最壞狀況時間複雜度和平均狀況時間複雜度。

最好狀況時間複雜度就是,在最理想的狀況下,執行這段代碼的時間複雜度。就像上面的示例,在最理想的狀況下,要查找的變量x正好是數組的第一個元素,這個時候對應的時間複雜度就是最好狀況時間複雜度 O(1)。

最壞狀況時間複雜度就是,在最糟糕的狀況下,執行這段代碼的時間複雜度。就像上面的示例,若是數組中沒有要查找的變量x,須要把整個數組都遍歷一遍才行,因此這種最糟糕 狀況下對應的時間複雜度就是最壞狀況時間複雜度 O(n)。

最好狀況時間複雜度和最壞狀況時間複雜度對應的都是極端狀況下的代碼複雜度,發 生的機率其實並不大。爲了更好地表示平均狀況下的複雜度,就出現了平均狀況時間複雜度的概念。那平均狀況時間複雜度如何分析呢?以上面的那段代碼爲例。 要查找的變量 x在數組中的位置,有 n+1 種狀況:在數組的 0~n-1 位置中和不在數組中。把每種狀況下,查找須要遍歷的元素個數累加起來,而後再除以 n+1,就能夠獲得須要遍歷的元素個 數的平均值,即:

在上面的學習中,咱們知道時間複雜度的大O標記法中,能夠省略掉係數、低階、常量,因此,我們把剛剛這個公 式簡化以後,獲得的平均時間複雜度就是 O(n)。

空間複雜度分析

空間複雜度相對時間複雜度來講就簡單不少了,空間複雜度也不是用來計算程序實際佔用的空間的。空間複雜度是對一個算法在運行過程當中臨時佔用存儲空間大小的一個量度。空間複雜度比較經常使用的有:O(1)、O(n)、O(n²),咱們一塊兒來看看這幾種經常使用的空間複雜度。

空間複雜度 O(1)

空間複雜度 O(1) 說明臨時開闢的內存空間跟變量n沒有關係,不會隨着n的變化而變化。例以下面這段代碼。

public void sum(int n) {
    int sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    System.out.println(sum);
}
複製代碼

雖然上面的這段代碼有變量n,可是在循環的時候並無開闢新的內存空間。因此這是的空間複雜度爲 O(1)。

空間複雜度 O(n)

空間複雜度爲 O(n) 說明在執行代碼的過程當中,開闢的臨時空間大小跟n成正比的關係,例以下面這段代碼.

public void array(int n) {
    int[] array = new int[n];
    for (int i = 1; i <= n; i++) {
        array[i] = i;
    }
}
複製代碼

這段代碼中新new了一個大小爲narray數組,因此這段代碼的空間複雜度爲O(n)。

空間複雜度 O(n²)

空間複雜度 O(n²) 就是在代碼的執行過程當中新開闢了一個二維列表,以下面這段代碼。

public void array(int n) {
    int[][] array = new int[n][n];
    for (int i = 1; i <= n; i++) {
        for (int j=0;j<n;j++) {
            array[i][j] = j;
        }
    }
}
複製代碼

以上,就是對算法的時間複雜度與空間複雜度的分析,歡迎你們一塊兒交流。

最後

打個小廣告,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,一塊兒進步吧。

平頭哥的技術博文

參考資料

  • 數據結構與算法之美(極客時間)
相關文章
相關標籤/搜索