數據結構與算法學習--複雜度分析

什麼是複雜度分析

  1. 數據結構和算法解決是「如何讓計算機更快時間、更省空間的解決問題」。
  2. 所以需從執行時間和佔用空間兩個維度來評估數據結構和算法的性能。
  3. 分別用時間複雜度和空間複雜度兩個概念來描述性能問題,兩者統稱爲複雜度。
  4. 複雜度描述的是 算法執行時間(或佔用空間)與數據規模的增加關係

爲何須要複雜度分析

  1. 和性能測試相比,複雜度分析有不依賴執行環境、成本低、效率高、易操做、指導性強的特色。
  2. 掌握複雜度分析,將能編寫出性能更優的代碼,有利於下降系統開發和維護成本。

如何進行復雜度分析

對於時間複雜度的分析,一般使用大O複雜度表示法,表示代碼執行時間隨數據規模增加的變化趨勢,因此,也叫做漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度web

用公式表示,就是 T(n) = O(f(n))表示,其中 T(n) 表示算法執行總時間,f(n) 表示每行代碼執行總次數,而 n 表示數據的規模。算法

因爲時間複雜度描述的是算法執行時間與數據規模的增加變化趨勢,因此常量階、低階以及係數實際上對這種增加趨勢不產決定性影響,因此在作時間複雜度分析時能夠忽略這些項。數組

具體分析的時候,有下列三個方法:數據結構

  1. 單段代碼只看循環次數最多的部分
  2. 多段代碼取複雜度最高的:即有個多個循環,但只看循環次數量級最高的那段代碼
  3. 乘法法則--嵌套代碼進行乘積:多個循環嵌套,就是相乘

常見的時間複雜度

按照數量級遞增,常見的時間複雜度量級有:數據結構和算法

  • 常量階 O(1)
  • 對數階 O(logn)
  • 線性階 O(n)
  • 線性對數階 O(nlogn)
  • 平方階 O(n^2),立方階 O(n^3)...k次階 O(n^k)
  • 指數階 O(2^n)
  • 階乘階 O(n!)

其中,最後兩種狀況是很是糟糕的狀況,固然 O(n^2) 也是一個能夠繼續進行優化的狀況。編輯器

接下來簡單介紹上述複雜度中的幾種比較常見的:函數

O(1)

O(1) 表示的是常量級時間複雜度,也就是只要代碼的執行時間不隨 n 的增大而增加,都記做 O(1) 。通常只要算法不包含循環語句和遞歸語句,時間複雜度都是 O(1)性能

像下列代碼,有 3 行,但時間複雜度依然是O(1),而非 O(3)測試

a = 3
b = 4
print(a + b)
O(logn)、O(nlogn)

O(logn) 也是一個常見的時間複雜度,下面是一個 O(logn) 的代碼例子:優化

i = 1
count = 0
n = 20
while i <= n:
    count += 1
    i *= 2
print('while 循環運行了 {} 次'.format(count))

這段代碼其實就是每次循環都讓變量 i 乘以 2,直到其大於等於 n,這裏我設置 n=20,而後運行了後,輸出結果是循環運行了 5 次。

實際上這段代碼的結束條件,就是求 2^x=n 中的 x 是等於多少,那麼循環次數也就知道了,而求 x 的數值,方法就是 x = log_2 n ,那麼時間複雜度就是 O(log_2n)

假如上述代碼進行簡單的修改,將 i *= 2 修改成 i *= 3 ,那麼同理能夠獲得時間複雜度就是 O(log_3 n)

但在這裏,不管是以哪一個爲對數的底,咱們都把對數階的時間複雜度記爲 O(logn)

這裏主要緣由有兩個:

  1. 對數能夠互換,好比 log_3n = log_3 2 * log_2n,也就是 O(log_3n)=O(C*log_2n),常量 C=log_32
  2. 基於前面的理論,係數能夠被忽略,也就是這裏的常量 C 能夠忽略

基於這兩個緣由,對數階的時間複雜度都忽略了底,統一爲 O(logn)

至於 O(nlogn) ,根據乘法法則,只須要將對數階複雜度的代碼,運行 n 次,就能夠獲得這個線性對數階複雜度了。

注意, O(nlogn) 是很是常見的時間複雜度,經常使用的排序算法如歸併排序、快速排序的時間複雜度都是 O(nlogn)

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

前面介紹的狀況都是隻有一個數據規模 n ,但這裏介紹有兩個數據規模的狀況--mn

# O(m+n)
def cal(n, m):
    result = 0
    for i in range(n):
        result += i

    for j in range(m):
        result += j * 2

    return result

簡單的代碼示例如上述所示,若是事先沒法評估 mn 的量級大小,那麼這裏的時間複雜度就無法選擇量級最大的,因此其時間複雜度就是 O(m+n)

同理,對於嵌套循環,就是 O(m*n) 的時間複雜度了。

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

這四種複雜度的定義以下:

  • 最好狀況時間複雜度:代碼在 最理想的狀況下執行的時間複雜度;
  • 最壞狀況時間複雜度:代碼在 最壞狀況下執行的時間複雜度;
  • 平均狀況時間複雜度:代碼在全部狀況下執行的次數的 加權平均值表示
  • 均攤時間複雜度:代碼執行的全部複雜度狀況中, 絕大多數都是低級別的複雜度,個別狀況會發生最高級別複雜度且發生具備時序關係時,能夠將個別高級別複雜度均攤到低級別複雜度上。基 本上均攤複雜度就等於低級別複雜度,也能夠看做是特殊的平均時間複雜度。

爲何會有這四種複雜度呢?緣由是:

同一段代碼在不一樣狀況下時間複雜度會出現量級差別,爲了更全面、更準確描述代碼的時間複雜度,引入這四種複雜度的概念;

但一般除非代碼是出現量級差異的時間複雜度,才須要區分這四種複雜度,大多數狀況都不須要區分它們。

下面是給出第一個代碼例子:

# 在數組 arr 中查找目標數值 x
def find(arr, x):
    for val in arr:
        if val == x:
            return True
    return False

這個例子假設數組 arr 的長度是 n ,那麼它最好的狀況,就是第一個數值就是須要查找的 x ,此時複雜度是 O(1) ,但最壞狀況就是最後一個數值或者不存在須要查找的 x ,那麼此時就遍歷一遍數組,複雜度就是 O(n) ,所以這段代碼最好和最壞狀況是會出現量級差異的,O(1)O(n) 分別是最好狀況複雜度和最壞狀況複雜度。

而這段代碼的平均狀況時間複雜度是 O(n) ,具體分析就是首先考慮全部可能的狀況以及對應出現的機率,可能發生的狀況先分爲兩種,存在和不存在須要查找的數值 x ,也就是分別是 1/2 的機率,而後對於存在的狀況下,又有 n 種狀況,即出如今數組任意位置的機率都是均等的,那麼它們的機率乘以存在的機率就是 1/2n ,接着再考慮每種狀況須要搜索的元素個數,其實就是代碼執行的次數,這個分別就是從 1 到 n,而且對於不存在的狀況,也是 n ,須要遍歷一遍數組才發現不存在,因此平均時間複雜度的計算過程以下:

1\times\frac{1}{2n}+2\times\frac{1}{2n}+\cdots+n\times\frac{1}{2n}+n\times\frac{1}{2} \\
= \frac{3n+1}{4}
\\

計算獲得的就是機率論中的加權平均值,也叫指望值,因此平均時間複雜度的全稱應該叫加權平均時間複雜度或者指望時間複雜度

這裏用大 O 表示法表示,而且去掉常量和係數後,就是 O(n)。

最後介紹下均攤時間複雜度,須要知足如下兩個條件才使用:

1)代碼在絕大多數狀況下是低級別複雜度,只有極少數狀況是高級別複雜度

2)低級別和高級別複雜度出現具備時序規律均攤結果通常都等於低級別複雜度

空間複雜度分析

和時間複雜度的定義相似,空間複雜度全稱就是漸進空間複雜度(asymptotic space complexity),表示算法的存儲空間與數據規模之間的增加關係

簡單介紹下一個程序所須要的空間主要由如下幾個部分構成:

  • 指令空間:是值用來存儲 通過編譯以後的程序指令所須要的空間。
  • 數據空間:是指用來存儲 全部常量和變量值所需的空間。其主要由兩個部分構成:
    • 存儲常量和簡單變量所須要的空間
    • 存儲複合變量所須要的空間。這一類空間包括數據結構所須要的動態分配的空間
  • 環境棧空間:用來保存 函數調用返回時恢復運行所須要的信息。例如,若是函數 fun1 調用了函數 fun2,那麼至少必須保存 fun2 結束時 fun1 將要繼續執行的指令的地址。

參考:

  • 極客時間數據結構課程
相關文章
相關標籤/搜索