時間複雜度的定義,記號以及幾種計算方法
時間複雜度在關心些什麼
如果接觸過演算法,那麼對於演算法的時間複雜度分析一定不陌生,因為時間複雜度是演算法優劣的一個重要評價標準。
對於一個演算法,我們不光關心它在某個規模輸入的情況下耗時多少,我們更關心的是,當輸入規模瘋狂增長的時候,演算法耗時增加多少,是按線性形式增加的?指數增加的?對數形式增加的?比如當你的演算法接收的輸入規模從k個數變到nk個數時,你的耗時是增加了n倍(線性速度增加)?還是增加了n2倍(平方的速度)?還是增加了log(n)倍(對數形式增加)。
時間複雜度是一個函式,它描述了演算法的執行時間,並且給出了執行時間與輸入規模之間的關係。
時間複雜度的分析記號:O(), Ω(), Θ(),o(), w()
對於時間複雜度的分析,我們都是使用漸進分析,這種分析方法關注地核心就是演算法耗時會如何隨著輸入規模增長?我們使用三種記號來說明這個問題。
我們使用字母n來表示輸入的規模
1. O()
符號O()表示了一種上界的感覺,也是我們常說的大O表示法,O()在這三種符號中用得最多,因為涉及到時間複雜度分析,我們經常會考慮最壞情況,因為在程式碼的實際運用中,首先最壞情況發生的可能性還不小,其次我們需要衡量最壞情況發生的結果我們是否能接受。
它的定義也用到了極限的思想,因為漸進分析,就包含有當n趨於無窮的意思。如果學過數學分析,瞭解ε-Δ語言,那麼一定對這塊兒的定義不會陌生。
g(n) ϵ O(f(n)): 存在常數c和n0
這個符號內含的意義就是,當我規模變得很大的時候,規模再怎麼增大,我的耗時一定能被一個函式(f(n))乘一個常數( c )控制住。
例子:2n2+27 = O(n2): 我們找到n0 = 10,c = 3,很容易驗證對所有n > n0我們都有2n2+27 < 3n2
2. Ω()
符號Ω()表示了一種下界的感覺,和O()定義差不多,只是不等號改變了。
g(n) ϵ Ω(f(n)): 存在常數c和n0
這個符號內含的意義就是,當我規模變得很大的時候,規模再怎麼增大,我的耗時一定比這個下界函式(f(n))乘一個常數( c )控制住,不管怎麼樣,總存在一個規模使得程式耗時會超過這個下界的。
例子:2n2+27 = Ω(n2): 我們找到n0 = 1,c = 1,很容易驗證對所有n > n0我們都有2n2+27 > n2
3. Θ()
符號Θ()表示了一種緊的感覺,他包含了O()和Ω(),把一個函式上下界都固定了邊界,在n趨於很大規模的時候,形成了一個控制帶控住了這個函式。
g(n) ϵ Θ(f(n)): g(n) ϵ O(f(n))並且g(n) ϵ Ω(f(n))
這個符號內含的意義就是,當我規模變得很大的時候,規模再怎麼增大,我的耗時一定比這個下界函式(f(n))乘一個常數( c )控制住,不管怎麼樣,總存在一個規模使得程式耗時會超過這個下界的。
例子:2n2+27 = Θ(n2): 因為我們之前使用的例子表示了n2既是2n2+27的上界也是下界,找這個上下界的時候n0和c可以找的不一樣(上界對應的n0和c是10和3,下界對應的n0和c是1和1,兩者不需要統一)
4. o()
符號o()表示了一種強上界的感覺,和O()定義差不多,只是條件更加嚴格了,對於所有c>0,那個上界都是上界。
g(n) ϵ o(f(n)): 存在n0 > 0,任取c>0,對所有的n > n0,我們有g(n) ≤ cf(n)。
這個符號內含的意義就是,當達到一定規模後,我的耗時永遠都超不過上界函式(f(n)),不管c怎麼幫我,c取0.01,0.000000001,耗時最後還是會比這個上界函式少,這是由於我的增長速度本質上就是慢過這個上界函式。
例子:2n2+27 = o(n3): n2的增長速度本質上就是比n3慢。我們最後找出的n0會是c的函式。
5. w()
符號o()表示了一種強下界的感覺,和Ω()定義差不多,只是條件更加嚴格了,對於所有c>0,那個下界都是下界。
g(n) ϵ w(f(n)): 存在n0 > 0,任取c>0,對所有的n > n0,我們有g(n) ≥ cf(n)。
這個符號內含的意義就是,當達到一定規模後,我的耗時總會超過這個下界函式(f(n)),不管c怎麼幫它,c取100,100000000,耗時最後還是會比這個下界函式多,這是由於我的增長速度本質上就是快過這個下界函式。
例子:2n2+27 = w(n): n2的增長速度本質上就是比n快。我們最後找出的n0會是c的函式。
時間複雜度的計算方法
對於演算法的耗時的公式,一般不會像上面那樣的例子直接給你一個清楚的2n2+27這種清晰的公式。比如許多時候我們會遇到遞迴這種情況,就會出現演算法耗時的遞推公式。
例子1:選擇排序
給n個沒排好序的數,我們從這列數先選出最小的那個排最前面,然後再從剩下沒排好序的數繼續選最小的排,持續下去。這種遞迴每次問題規模減小1,所以我們有耗時的遞推公式如下:T(n)=T(n-1)+cn,我的n個數排序耗時等於我選好個最小的時間加上剩下n-1個數進行排序,從n個數裡選最小的數耗時cn因為我們要瀏覽這n個數。
例子2:歸併排序
對於給n個沒排好序的數排序,我們可以把它平均拆解成兩份,每份是n/2個數,再對這n/2個數進行排序,排好後,我們對左右兩邊進行融合,這需要瀏覽這n個數才可以按順序融合好。所以耗時遞推公式如下:T(n) = T(n/2) + cn
對於這些有遞推公式的時間複雜度的計算,有如下方法可以解決:
1.猜想並用數學歸納法證明
2.迭代,比如T(n)=T(n-1)+cn,把n替換成n-1,我們也知道T(n-1) = T(n-2)+c(n-1),所以我們可以把T(n)寫開:
T(n)=T(n-1)+cn = T(n-2)+c(n-1)+ cn = … = T(1) + c*2+… + c(n-1)+cn
這樣就可把T(n)一直迭代下去,到表達成我們已知的資訊,比如T(1)
3.藉助遞迴樹,比如T(n) = 2*T(n/2) + cn,我們可以把這個遞迴表達成如下樹結構:
T(n)是這棵樹所有結點值的總和,他等於根結點的值加左右子樹的所有結點值總和,而左右子樹和原樹的區別就是n變成了n/2。所以有T(n) = 2T(n/2)(這是左右子樹結點值的總和)+cn(這是根結點的值)。
注意到這樹的每一層加起來都是cn,所以T(n) = cn × 這個樹的高度。這個樹一直會分叉到1,所以高度是log2(cn),因為高度表示的就是cn需要分叉幾次到1,就是對cn求對數。
所以T(n) = cn × log2(cn),T(n) = Θ(nlog2n)
更一般的:T(n) = aT(n/b) + cnk, T(1) = c,
a ≥ 1, b > 1, c > 0 and k ≥ 0
我們也可以畫出如下遞迴樹後算出T(n)的值
同樣也是每行相加都呈現一個規律性的值,再對T(n)進行分析,最後有如下結論:
對於T(n) = aT(n/b) + cnk, T(1) = c,
a ≥ 1, b > 1, c > 0 and k ≥ 0
有:
1.當a < bk, T(n) = Θ(nk)
2.當a = bk, T(n) = Θ(nklogn)
3.當a > bk, T(n) = Θ(n^logba)
總結
1.我們首先談及了時間複雜度的定義,我們最關心的就是演算法的耗時增長的速率,是呈什麼函式形式增長的。
2.接下來我們通過符號,將時間複雜度的表示進行了量化,包括給了上界,下界,緊,強上界,強下界的這些類似概念,很好地區刻畫了一個演算法的時間隨著規模增長的形式和規律。
3.我們對演算法的時間複雜度計算方法進行了一個初步的介紹,並對遞迴的演算法給出了一個綜合性的公式。