算法複雜度分析

什麼是算法?

算法(algorithm)是對特定問題求解步驟的一種描述,它是指令的有限序列,其中每一條指令表示一個或多個操做;此外,一個算法一般來講具備如下五個特性:java

  • 輸入:一個算法應以待解決的問題的信息做爲輸入。
  • 輸出:輸入對應指令集處理後獲得的信息。
  • 有窮性:算法執行的指令個數是有限的,每一個指令又是在有限時間內完成的,所以整個算法也是在有限時間內能夠結束的。
  • 可行性:算法是可行的,即算法中的每一條指令都是能夠實現的,均能在有限的時間內完成。
  • 肯定性:算法對於特定的合法輸入,其對應的輸出是惟一的。即當算法從一個特定輸入開始,屢次執行同一指令集結果老是相同的。

算法效率的度量

過後統計法

這種方法有兩個缺陷:一是必須先運行一句算法編制的程序;二是所得時間的統計量依賴於計算機的硬件、軟件、等環境因素,有時容易掩蓋算法自己的優點。故通常採用事前估算的方法。算法

事前分析估算法

一個算法是由控制結構(順序、分支和循環3種)和原操做(指固有數據類型的操做)構成的,則算法時間取決於二者的綜合效果。爲了便於比較同一個問題的不一樣算法,一般的作法是,從算法中選取一種對於所研究的問題(或算法類型)來講是基本操做的原操做,以該基本操做的重複執行的次數做爲算法的時間量度。

一個用高級程序語言編寫的程序在計算機上運行式所消耗的時間取決於下列因素:數組

  1. 算法採用的策略、方法
  2. 問題的規模
  3. 書寫程序的語言
  4. 編譯程序所產生的機器代碼的質量
  5. 機器執行指令的速度

時間複雜度分析

漸進時間複雜度(asymptotic time complexity):若存在函數f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限值爲不等於零的常數,則稱f(n)是T(n)的同量級函數。記做T(n)=O(f(n)),稱O(f(n))爲算法的漸進時間複雜度,簡稱 時間複雜度。漸進時間複雜度用大寫O來表示,因此也被稱爲 大O表示法

T(n)=O(f(n))這個公式中,T(n)表示代碼的執行時間;n表示數據規模的大小;f(n)表示每行代碼執行的次數總和。由於這是一個公式,因此用f(n)來表示,公式中的O表示漸進於無窮的一種行爲。大O時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增加的變化趨勢。微信

如何推導出時間複雜度呢?有以下幾個原則:

  1. 若是運行時間是常數量級,用常數1表示。
  2. 只保留時間函數中的最高階項
  3. 若是最高階項存在,則省去最高階項前面的係數。

或者換種說法:在」公式中的低階、常量、係數三部分並不左右增加趨勢,因此均可以忽略「基礎上,數據結構

  1. 只關注循環執行次數最多的一段代碼
  2. 加法法則:總複雜度等於量級最大的那段代碼的複雜度

    若是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)))性能

  3. 乘法法則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積

    若是 T1(n)=O(f(n)),T2(n)=O(g(n));測試

    那麼 T(n)=T1(n)T2(n)=O(f(n))O(g(n))=O(f(n)g(n));code

幾種常見時間複雜度

按數量級遞增:
常量階 O(1)
對數階 O(logn)
線性階 O(n)
線性對數階 O(nlogn)
平方階 O(n^2)、立方階 O(n^3)、...k次方階 O(n^k)
指數階 O(2^n)
階乘階 O(n!)

對於上述羅列的複雜度量級,能夠分爲:多項式量級非多項式量級。其中,非多項式量級只有兩個:指數階 O(2n)和階乘階 O(n!)。排序

咱們把時間複雜度爲非多項式量級的算法問題叫做NP(Non-Deterministic Polynomial,非肯定多項式)問題。當數據規模 n 愈來愈大時,非多項式量級算法的執行時間會急劇增長,求解問題的執行時間會無限增加。因此,非多項式時間複雜度的算法實際上是很是低效的算法。

下面結合簡單的代碼分析幾種常見的多項式時間複雜度:

O(1)

public void fun(){//沒有入參變量
    int sum = 0;//執行次數:1
    int p = 1;//執行次數:1
    for(; p <= 100; ++p){//執行次數爲:100
        sum = sum + p;//執行次數爲:100
    }
}

這段代碼的時間複雜度爲O(1)。這段代碼執行了100次,是一個常量的執行時間,跟n的規模無關。即只要代碼的執行時間不隨n的增加而增加,代碼的時間複雜度都記做爲O(1)。

O(n)

public void fun(){
    int n = 100;//執行次數:1
    int sum = 0;//執行次數:1
    for(int j = 1; j <= n; ++j){//執行次數:n
        sum += j;//執行次數:n
    }
}

即T(n)=1+1+n+n=2n+2,根據前面說到的算法分析原則,當n趨向於無窮大時,能夠忽略低階項和最高項係數,則時間複雜度爲O(n)。

O(logn)、O(nlogn)

int i = 1;
while (i <= n) {
    i = i * 2;
}

實際上,咱們只需找出循環執行次數最多的代碼,求出該代碼被執行了多少次,就能知道整段代碼的時間複雜度。從代碼中能夠看出,變量i的值從1開始,每循環一次乘以2,實際上i的取值是一個等比數列。經過2x=n,求出x=log2n,因此這段代碼的時間複雜度爲O(log2n)。

int i = 1;
while (i <= n) {
    i = i * 3;
}

根據上述思路,得出這段代碼的時間複雜度爲O(log3n))。

實際上,不管是以 2 爲底、以 3 爲底,仍是以 10 爲底,咱們能夠把全部對數階的時間複雜度都 記爲 O(logn)。

咱們知道,對數之間是能夠互相轉換的,log n 就等於 log 2 log n,因此 O(log n) = O(C log n),其中 C=log 2 是一個常量。基於咱們前面的一個理論:在採用大 O 標記複雜度的時 候,能夠忽略係數,即 O(Cf(n)) = O(f(n))。因此,O(log n) 就等於 O(log n)。所以,在對數階 時間複雜度的表示方法里,咱們忽略對數的「底」,統一表示爲 O(logn)。

若是一段代碼的時間複雜度是 O(logn),咱們循環執行 n 遍,時間複雜度就是 O(nlogn) 了。並且,O(nlogn) 也是一種很是常見的算法時間複雜度。好比,歸併排序、快速排序的時間複雜度都是 O(nlogn)。

O(n2)

public void fun(){
    int n = 100;//執行次數:1
    int sum = 0;//執行次數:1
    for(int i = 1; i <= n; ++i){//執行次數:n
        for(int j = 1; j <= n; ++j){//執行次數n*n
            sum += j;//執行次數n*n
        }
    }
}

根據乘法規則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積。故時間複雜度爲O(n2)。

O(m+n)

public int fun(int m, int n){
    int sum1 = 0;
    int i = 1;
    for(; i < m; ++i){
        sum1 = sum1 + i;
    }
    
    int sum2 = 0;
    int j = 1;
    for(; j < n; ++j){
        sum2 = sum2 + j;
    }
    return sum1 + sum2;
}

從代碼中能夠看出,m 和 n 是表示兩個數據規模。咱們沒法事先評估 m 和 n 誰的量級大,因此咱們在表示複雜度的時候,就不能簡單地利用加法法則,省略掉其中一個。因此,上面代碼的時間複雜度就是 O(m+n)。

空間複雜度分析

空間複雜度(Space Complexity)是對一個算法在運行過程當中臨時佔用存儲空間大小的量度。

算法的空間複雜度經過計算算法所需的存儲空間實現,算法的空間複雜度的計算公式記做:S(n)=O(f(n)),其中,n爲問題的規模,f(n)爲語句關於n所佔存儲空間的函數。

程序執行時所需存儲空間包括如下兩部分。  
(1)固定部分。這部分空間的大小與輸入/輸出的數據的個數多少、數值無關。主要包括指令空間(即代碼空間)、數據空間(常量、簡單變量)等所佔的空間。這部分屬於靜態空間。
(2)可變空間,這部分空間的主要包括動態分配的空間,以及遞歸棧所需的空間等。這部分的空間大小與算法有關。

一個算法的空間複雜度只考慮在運行過程當中爲局部變量分配的存儲空間的大小,它包括爲參數表中形參變量分配的存儲空間和爲在函數體中定義的局部變量分配的存儲空間兩個部分。

public void fun(int n){
    int[] a = new int[n];
    for(int i = 1; i < n; ++i){
        a[i] = i * i;
    }
}

從代碼中能夠看出,申請了一個空間存儲變量i,可是它是常量階的,跟數據規模n無關。第二行代碼爲申請了一個大小爲n的int類型數組,除此以外,沒有佔用更多的空間,因此整段代碼的空間複雜度就是O(n)。

最好、最壞、平均狀況時間複雜度

最好狀況時間複雜度:代碼在最理想狀況下執行的時間複雜度。

最壞狀況時間複雜度:代碼在最壞狀況下執行的時間複雜度。

平均狀況時間複雜度:用代碼在全部狀況下執行的次數的加權平均值表示,簡稱平均時間複雜度。

public int find(int[] array, int n. int x){
    int index = -1;
    for(int i = 0; i < n; ++i){
        if(array[i] == x){
            index = i;
            break;
        }
    }
    return index;
}

在這段代碼中,若是要查找的變量x出如今第一個元素,那就不須要繼續遍歷剩下的n-1個數據了,那麼時間複雜度就是O(1),爲最好狀況時間複雜度。但若是數組中不存在變量x,那咱們就須要把整個數組遍歷一遍,時間複雜度就成O(n),爲最壞狀況時間複雜度。故在不一樣狀況下代碼的時間複雜度是不同的。

最好狀況時間複雜度和最壞狀況時間複雜度都是在極端狀況下的代碼時間複雜度,發生的機率不大。爲了更好地表示平均狀況下的複雜度,咱們須要引入一個新的概念:平均時間複雜度

在這段代碼中,要查找的變量x在數組中的位置有n+1中狀況:在數組的0~n-1位置中和不在數組中。假設在數組中和不在數組中的機率都爲1/2;變量x出如今0~n-1這n個位置的機率爲1/n,根據機率乘法規則,要查找的數據出如今0~n-1中任意位置的機率爲1/(2n)。故平均時間複雜度的計算爲:(查找須要遍歷的元素個數乘以相應的權術)

$$ 1*1/(2n)+2*1/(2n)+3*1/(2n)+···+n*1/(2n)+n*1/2=(3n+1)/4 $$

這個值爲加權平均值,也叫指望值。因此平均時間複雜度的全稱應該叫加權平均時間複雜度或者指望時間複雜度。故這段代碼的平均時間複雜度爲O(n)。

加權平均值即將各數值乘以相應的權數,而後加總求和獲得整體值,再除以總的單位數。

在大多數狀況下,咱們並不須要區分最好、最壞、平均狀況時間複雜度三種狀況。咱們使用一個複雜度就能夠知足需求了。只有同一塊代碼在不同的狀況下,時間複雜度有量級的差距,咱們纔會使用這三種複雜度表示法來區分。

均攤時間複雜度

對一個數據結構進行一組連續操做中,大部分狀況下時間複雜度都很低,只有個別狀況下時間複雜度比較高,並且這些操做之間存在先後連貫的時序關係,這個時候,咱們就能夠將這一組操做放在一起分析,看是否能將較高時間複雜度那次操做的耗時,平攤到其餘那些時間複雜度比較低的操做上。並且,在可以應用均攤時間複雜度分析的場合,通常均攤時間複雜度就等於最好狀況時間複雜度。

請看下面這段代碼:

int[] array = new int[10];//聲明一個大小爲10的數組array
int length = 10;
int i = 0;
//往數組裏添加一個元素
public void addElement(int element){
    if(i > = len){
        int[] new_array = new int[len*2];
        for(int j = 0; j < len; ++j){
            //複製數據到new_array數組中
            new_array[j] = array[j];
        }
        array = new_array;
        len = len * 2;
    }
    //將element插入到下標爲i的位置
    array[i] = element;
    ++i;
}

在這段代碼中,當i<len時,不走for循環,因此時間複雜度爲O(1);

當i>=len時,即當i=n時,for循環進行數組的複製,只有這一次的時間複雜度爲O(n)。

由此可知:

  • 該算法的最好狀況時間複雜度爲O(1);
  • 最壞狀況時間複雜度爲O(n);
  • 平均狀況時間複雜度爲:

    $$ 1*1/(n+1)+1*1/(n+1)+···+1*1/(n+1)+n*1/(n+1)=2n/(n+1) $$

    故平均時間複雜度爲O(1)。

  • 均攤複雜度:前n個操做複雜度都是O(1),第n+1次操做的複雜度是O (n),因此把最後一次的複雜度分攤到前n次上,那麼均攤下來每次操做的複雜度爲O(1)。

總結:漸進式時間,空間複雜度分析只是一個理論模型,只能提供給粗略的估計分析,一個低階的時間複雜度程序有極大的可能性會優於一個高階的時間複雜度程序,針對不同的宿主環境,不同的數據集,不同的數據量的大小,在實際應用上面可能真正的性能會不同。針對不同的實際狀況, 進而進行必定的性能基準測試是頗有必要的,進而選擇適合特定應用場景下的最優算法。

文章同步在微信公衆號,習慣微信上看文章的能夠關注微信公衆號:加二減壹
相關文章
相關標籤/搜索