數據結構與算法之美--01時間複雜度分析

複雜度分析上

聽了極客時間的數據結構與算法之美課程後,受益匪淺,把一些我認爲比較重要的知識點總結下來,雖然大部分是複製粘貼的。。。。。

數據結構與算法之美音頻文件,需要的小夥伴們可以下載啊~

**時間複雜度:**大O時間複雜度實際上並不具體表示代碼真正的執行 時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。

時間複雜度分析:

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

  • 加法法則:總複雜度等於量級最大的那段代碼的複雜度

    int cal(int n) {
    
    int sum_1 = 0; int p = 1; for (; p < 100; ++p) { sum_1 = sum_1 + p; }
    
    int sum_2 = 0; int q = 1; for (; q < n; ++q) { sum_2 = sum_2 + q; }
    
    int sum_3 = 0; int i = 1; int j = 1; for (; i <= n; ++i) {
    
    j = 1;
    
    for (; j <= n; ++j) {
    
    sum_3 = sum_3 + i * j;
    
    } }
    
    return sum_1 + sum_2 + sum_3;
    
    }
    
    • 即便這段代碼循環 10000 次、 100000 次,只要是一個已知的數,跟 n 無關,照樣也是常量級的執行時間。當 n 無限大的時候,就可以忽略。盡 管對代碼的執行時間會有很大影響,但是回到時間複雜度的概念來說,它表示的是一個算法執行效率與數據規模增長的變化趨勢,所以不管常量的執行時間多 大,我們都可以忽略掉。因爲它本身對增長趨勢並沒有影響。
    • 那第二段代碼和第三段代碼的時間複雜度是多少呢?答案是O(n)和O(n 2 )
  • 乘法法則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積

    如果 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)).

    int cal(int n) {
    
            int ret = 0; 
            int i = 1; 
            for (; i < n; ++i) {
                ret = ret + f(i);
            } 
        }
    
        int f(int n) { 
            int sum = 0; 
            int i = 1;
            for (; i < n; ++i) { 
                sum = sum + i; 
            } 
            return sum;
        }
    

    我們單獨看 cal() 函數。假設 f() 只是一個普通的操作,那第 4 ~ 6 行的時間複雜度就是, T1(n) = O(n) 。 O(n),所以,整個cal()函數的時間複雜度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n 2 )。

常用複雜度量級:

在這裏插入圖片描述

我們可以粗略地分爲兩類,多項式量級和非多項式量級。其中,非多項式量級只有兩個:O(2 n )和O(n!)。 當數據規模 n 越來越大時,非多項式量級算法的執行時間會急劇增加,求解問題的執行時間會無限增長。所以,非多項式時間複雜度的算法其實是非常低效的算 法。我們主要來看幾種常見的多項式時間複雜度。

O(1):

O(1) 只是常量級時間複雜度的一種表示方法,並不是指只執行了一行代碼。比如這段代碼,即便有 3 行,它的時間複雜度也是 O(1 ), 而不是 O(3) 。

int i = 8;
int j = 6; 
int sum = i + j;

只要代碼的執行時間不隨n的增大而增長,這樣代碼的時間複雜度我們都記作O(1)。或者說,一般情況下,只要算法中不存在循環語句、遞歸語 句,即使有成千上萬行的代碼,其時間複雜度也是Ο(1)。

O(logn)、O(nlogn)

對數階時間複雜度非常常見,同時也是最難分析的一種時間複雜度。

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

根據前面講的複雜度分析方法,第三行代碼是循環執行次數最多的。

從代碼中可以看出,變量 i 的值從 1 開始取,每循環一次就乘以 2 。當大於 n 時,循環結束。還記得我們高中學過的等比數列嗎?實際上,變量 i 的取值就是一個等比 數列。如果我把它一個一個列出來,就應該是這個樣子的:

在這裏插入圖片描述

我們只要知道x值是多少,就知道這行代碼執行的次數了。通過2 x =n求解x這個問題我們想高中應該就學過了,我就不多說了。x=log 2 n,所以,這段代碼的 時間複雜度就是O(log 2 n)。

再看下面的代碼:

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

這段代碼的時間複雜度爲O(log 3 n)。

實際上,不管是以 2 爲底、以 3 爲底,還是以 10 爲底,我們可以把所有對數階的時間複雜度都記爲 O(logn) 。

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

如果一段代碼的時間複雜度是 O(logn) ,我們循環執行 n 遍,時間復 雜度就是 O(nlogn) 了。而且, O(nlogn) 也是一種非常常見的算法時間複雜度。比如,歸併排序、快速排序的時間複雜度都是 O(nlogn) 。

O(m+n)、O(m*n)

int cal(int m, int n) {
    int sum_1 = 0; 
    int i = 1; 
    for (; i < m; ++i) { 
        sum_1 = sum_1 + i; 
    }

    int sum_2 = 0; 
    int j = 1; 
    for (; j < n; ++j) { 
        sum_2 = sum_2 + j;
    }
    return sum_1 + sum_2;
}

從代碼中可以看出, m 和 n 是表示兩個數據規模。我們無法事先評估 m 和 n 誰的量級大,所以我們在表示複雜度的時候,就不能簡單地利用加法法則,省略掉其中一 個。所以,上面代碼的時間複雜度就是 O(m+n) 。 針對這種情況,原來的加法法則就不正確了,我們需要將加法規則改爲: T1(m) + T2(n) = O(f(m) + g(n)) 。但是乘法法則繼續有效: T1(m)*T2(n) = O(f(m) * f(n)) 。

空間複雜度分析

void print(int n) {

    int i = 0;
    int[] a = new int[n];
    for (i; i <n; ++i) {
        a[i] = i * i;
    }

    for (i = n-1; i >= 0; --i) {
        print out a[i]
    }
}

我們可以看到,第 2 行代碼中,我們申請了一個空間存儲變量 i ,但是它是常量階的,跟數據規模 n 沒有關係,所以我們可以忽略。第 3 行申 請了一個大小爲 n 的 int 類型數組,除此之外,剩下的代碼都沒有佔用更多的空間,所以整段代碼的空間複雜度就是 O(n) 。

我們常見的空間複雜度就是O(1)、O(n)、O(n 2 ),像O(logn)、O(nlogn)這樣的對數階複雜度平時都用不到。而且,空間複雜度分析比時間複雜度分析要簡單很多。

內容小結:

複雜度也叫漸進複雜度,包括時間複雜度和空間複雜度,用來分析算法執行效率與數據規模之間的增長關係,可以粗略地表示,越高階複雜度的算法,執行效率 越低。常見的複雜度並不多,從低階到高階有:O(1)、O(logn)、O(n)、O(nlogn)、O(n 2 )。
在這裏插入圖片描述

複雜度分析(下):淺析最好、最壞、平均、均攤時間複雜度

四個複雜度分析方面的知識點,最好情況時間複雜度(best case time complexity)、最壞情況時間複雜度(worst case time complexity)、平均 情況時間複雜度(average case time complexity)、均攤時間複雜度(amortized time complexity)。

最好、最壞情況時間複雜度

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

這段代碼要實現的功能是,在一個無序的數組( 段代碼的複雜度是 O(n) ,其中, n 代表數組的長度。

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 可能出現在數組的任意位置。如果數組中第一個元素正好是要查找的變量 x ,那就不需要繼續遍歷剩下的 n-1 個數據了,那時間複雜度就 是 O(1) 。但如果數組中不存在變量 x ,那我們就需要把整個數組都遍歷一遍,時間複雜度就成了 O(n) 。所以,不同的情況下,這段代碼的時間複雜度是不一樣的。

爲了表示代碼在不同情況下的不同時間複雜度,我們需要引入三個概念:最好情況時間複雜度、最壞情況時間複雜度和平均情況時間複雜度。

顧名思義,最好情況時間複雜度就是,在最理想的情況下,執行這段代碼的時間複雜度。就像我們剛剛講到的,在最理想的情況下,要查找的變量x正好是數組的第一個元素,這個時候對應的時間複雜度就是最好情況時間複雜度。

同理,最壞情況時間複雜度就是,在最糟糕的情況下,執行這段代碼的時間複雜度。就像剛舉的那個例子,如果數組中沒有要查找的變量x,我們需要把整個數組 都遍歷一遍才行,所以這種最糟糕情況下對應的時間複雜度就是最壞情況時間複雜度。

平均情況時間複雜度

要查找的變量x在數組中的位置,有n+1種情況:在數組的0~n-1位置中和不在數組中。我們把每種情況下,查找需要遍歷的元素個數累加起來,然後再除以n+1, 就可以得到需要遍歷的元素個數的平均值,即:

在這裏插入圖片描述

我們知道,要查找的變量 x ,要麼在數組裏,要麼就不在數組裏。這兩種情況對應的概率統計起來很麻煩,爲了方便你理解,我們假設在數組中與不在數組中的概 率都爲 1/2 。另外,要查找的數據出現在 0 ~ n-1 這 n 個位置的概率也是一樣的,爲 1/n 。所以,根據概率乘法法則,要查找的數據出現在 0 ~ n-1 中任意位置的概率就 是 1/(2n) 。

如果我們把每種情況發生的概率也考慮進去,那平均時間複雜度的計算過 程就變成了這樣:
在這裏插入圖片描述

這個值就是概率論中的加權平均值,也叫作期望值,所以平均時間複雜度的全稱應該叫加權平均時間複雜度或者期望時間複雜度。

引入概率之後,前面那段代碼的加權平均值爲(3n+1)/4。用大 O 表示法來表示,去掉係數和常量,這段代碼的加權平均時間複雜度仍然是O(n)。

均攤時間複雜度

//實現向數組中插入數據的功能
void insert(int val) {
    if (count == array.length) { 
        int sum = 0; 
        for (int i = 0; i < array.length; ++i) { 
            sum = sum + array[i]; 
        } 
        array[0] = sum;
        count = 1; 
    }
    array[count] = val; 
    ++count;
}

這段代碼實現了一個往數組中插入數據的功能。當數組滿了之後,也就是代碼中的count == array.length時,我們用for循環遍歷數組求和,並清空數組,將求和之後的 sum 值放到數組的第一個位置,然後再將新的數據插入。但如果數組一開始就有空閒空間,則直接將數據插入數組。

最理想的情況下,數組中有空閒空間,我們只需要將數據插入到數組下標爲 count 的位置就可以了,所以最好情況時間複雜度爲 O(1) 。

最壞的情況下,數組中沒有空閒空間了,我們需要先做一次數組的遍歷求和,然後再將數據插入,所以最壞情況時間複雜度爲 O(n) 。

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

假設數組的長度是 n ,根據數據插入的位置的不同,我們可以分爲 n 種情況,每種情況的時間複雜度是 O(1) 。除此之外,還有一種 “ 額外 ” 的情況,就是在數組沒有空 閒空間時插入一個數據,這個時候的時間複雜度是 O(n) 。而且,這 n+1 種情況發生的概率一樣,都是 1/(n+1) 。所以,根據加權平均的計算方法,我們求得的平均時 間複雜度就是:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章