复杂度分析(大O表示法)
復雜度分析
前文提要
本文完完全全引用極客時間的文章《數(shù)據(jù)結(jié)構(gòu)與算法之美》,作者王爭。
數(shù)據(jù)結(jié)構(gòu)是作為程序猿繞不過的一道坎,所以萌生了學習的想法,試讀了幾篇文章后發(fā)現(xiàn)講的很好,也有很多人訂閱,于是不回頭的走。只希望能不忘初心,不讓犯懶擊敗自己。
正文
為什么需要復雜度分析
你可能會有些疑惑,我把代碼跑一遍,通過統(tǒng)計、監(jiān)控,就能得到算法執(zhí)行的時間和占用的內(nèi)存大小。為什么還要做時間、空間復雜度分析呢?這種分析方法能比我實實在在跑一遍得到的數(shù)據(jù)更準確嗎?首先,我可以肯定地說,你這種評估算法執(zhí)行效率的方法是正確的。很多數(shù)據(jù)結(jié)構(gòu)和算法書籍還給這種方法起了一個名字,叫事后統(tǒng)計法。但是,這種統(tǒng)計方法有非常大的局限性。
測試結(jié)果非常依賴測試環(huán)境
測試環(huán)境中硬件的不同會對測試結(jié)果有很大的影響。
比如,我們拿同樣一段代碼,分別用 Intel Core i9 處理器和 Intel Core i3 處理器來運行,不用說,i9 處理器要比 i3 處理器執(zhí)行的速度快很多。還有,比如原本在這臺機器上 a 代碼執(zhí)行的速度比 b 代碼要快,等我們換到另一臺機器上時,可能會有截然相反的結(jié)果。
測試結(jié)果受數(shù)據(jù)規(guī)模的影響很大。
后面我們會講排序算法,我們先拿它舉個例子。對同一個排序算法,待排序數(shù)據(jù)的有序度不一樣,排序的執(zhí)行時間就會有很大的差別。極端情況下,如果數(shù)據(jù)已經(jīng)是有序的,那排序算法不需要做任何操作,執(zhí)行時間就會非常短。除此之外,如果測試數(shù)據(jù)規(guī)模太小,測試結(jié)果可能無法真實地反映算法的性能。比如,對于小規(guī)模的數(shù)據(jù)排序,插入排序可能反倒會比快速排序要快!所以,我們需要一個不用具體的測試數(shù)據(jù)來測試,就可以粗略地估計算法的執(zhí)行效率的方法。這就是我們今天要講的時間、空間復雜度分析方法。
大 O 復雜度表示法
算法的執(zhí)行效率,粗略地講,就是算法代碼執(zhí)行的時間。但是,如何在不運行代碼的情況下,用“肉眼”得到一段代碼的執(zhí)行時間呢?這里有段非常簡單的代碼,求 1,2,3…n 的累加和?,F(xiàn)在,我就帶你一塊來估算一下這段代碼的執(zhí)行時間。
假設每行代碼執(zhí)行的時間都一樣,為 unit_time。在這個假設的基礎之上,這段代碼的總執(zhí)行時間是多少呢?第 2、3 行代碼分別需要 1 個 unit_time 的執(zhí)行時間,第 4、5 行都運行了 n 遍,所以需要 2n*unit_time 的執(zhí)行時間,所以這段代碼總的執(zhí)行時間就是 *(2n+2)unit_time??梢钥闯鰜?#xff0c;所有代碼的執(zhí)行時間 T(n) 與每行代碼的執(zhí)行次數(shù)成正比。
按照這個分析思路,我們再來看這段代碼。
我們依舊假設每個語句的執(zhí)行時間是 unit_time。那這段代碼的總執(zhí)行時間 T(n) 是多少呢?
第 2、3、4 行代碼,每行都需要 1 個 unit_time 的執(zhí)行時間,第 5、6 行代碼循環(huán)執(zhí)行了 n 遍,需要 2n * unit_time 的執(zhí)行時間,第 7、8 行代碼循環(huán)執(zhí)行了 n2遍,所以需要 2n2 * unit_time 的執(zhí)行時間。
所以,整段代碼總的執(zhí)行時間 T(n) = (2n2+2n+3)*unit_time。盡管我們不知道 unit_time 的具體值,但是通過這兩段代碼執(zhí)行時間的推導過程,我們可以得到一個非常重要的規(guī)律,那就是,所有代碼的執(zhí)行時間 T(n) 與每行代碼的執(zhí)行次數(shù) n 成正比。我們可以把這個規(guī)律總結(jié)成一個公式。注意,大 O 就要登場了!
具體解釋一下這個公式。
其中,T(n) 我們已經(jīng)講過了,它表示代碼執(zhí)行的時間;n 表示數(shù)據(jù)規(guī)模的大小;f(n) 表示每行代碼執(zhí)行的次數(shù)總和。因為這是一個公式,所以用 f(n) 來表示。公式中的 O,表示代碼的執(zhí)行時間 T(n) 與 f(n) 表達式成正比。
所以,第一個例子中的 T(n) = O(2n+2),第二個例子中的 T(n) = O(2n2+2n+3)。這就是大 O 時間復雜度表示法。大 O 時間復雜度實際上并不具體表示代碼真正的執(zhí)行時間,而是表示代碼執(zhí)行時間隨數(shù)據(jù)規(guī)模增長的變化趨勢,所以,也叫作漸進時間復雜度(asymptotic time complexity),簡稱時間復雜度。
時間復雜度分析
只關(guān)注循環(huán)執(zhí)行次數(shù)最多的一段代碼
我剛才說了,大 O 這種復雜度表示方法只是表示一種變化趨勢。我們通常會忽略掉公式中的常量、低階、系數(shù),只需要記錄一個最大階的量級就可以了。所以,我們在分析一個算法、一段代碼的時間復雜度的時候,也只關(guān)注循環(huán)執(zhí)行次數(shù)最多的那一段代碼就可以了。這段核心代碼執(zhí)行次數(shù)的 n 的量級,就是整段要分析代碼的時間復雜度。
其中第 2、3 行代碼都是常量級的執(zhí)行時間,與 n 的大小無關(guān),所以對于復雜度并沒有影響。循環(huán)執(zhí)行次數(shù)最多的是第 4、5 行代碼,所以這塊代碼要重點分析。前面我們也講過,這兩行代碼被執(zhí)行了 n 次,所以總的時間復雜度就是 O(n)。
加法法則:總復雜度等于量級最大的那段代碼的復雜度
我這里還有一段代碼。你可以先試著分析一下,然后再往下看跟我的分析思路是否一樣。
這個代碼分為三部分,分別是求 sum_1、sum_2、sum_3。
我們可以分別分析每一部分的時間復雜度,然后把它們放到一塊兒,再取一個量級最大的作為整段代碼的復雜度。
第一段的時間復雜度是多少呢?這段代碼循環(huán)執(zhí)行了 100 次,所以是一個常量的執(zhí)行時間,跟 n 的規(guī)模無關(guān)。這里我要再強調(diào)一下,即便這段代碼循環(huán) 10000 次、100000 次,只要是一個已知的數(shù),跟 n 無關(guān),照樣也是常量級的執(zhí)行時間。當 n 無限大的時候,就可以忽略。盡管對代碼的執(zhí)行時間會有很大影響,但是回到時間復雜度的概念來說,它表示的是一個算法執(zhí)行效率與數(shù)據(jù)規(guī)模增長的變化趨勢,所以不管常量的執(zhí)行時間多大,我們都可以忽略掉。因為它本身對增長趨勢并沒有影響。
那第二段代碼和第三段代碼的時間復雜度是多少呢?答案是 O(n) 和 O(n2),你應該能容易就分析出來,我就不啰嗦了。綜合這三段代碼的時間復雜度,我們?nèi)∑渲凶畲蟮牧考墶K?#xff0c;整段代碼的時間復雜度就為 O(n2)。也就是說:總的時間復雜度就等于量級最大的那段代碼的時間復雜度。那我們將這個規(guī)律抽象成公式就是:
如果 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))).
乘法法則:嵌套代碼的復雜度等于嵌套內(nèi)外代碼復雜度的乘積
我剛講了一個復雜度分析中的加法法則,這兒還有一個乘法法則。類比一下,你應該能“猜到”公式是什么樣子的吧?如果 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)).
也就是說,假設 T1(n) = O(n),T2(n) = O(n2),則 T1(n) * T2(n) = O(n3)。落實到具體的代碼上,我們可以把乘法法則看成是嵌套循環(huán),我舉個例子給你解釋一下。
我們單獨看 cal() 函數(shù)。假設 f() 只是一個普通的操作,那第 4~6 行的時間復雜度就是,T1(n) = O(n)。但 f() 函數(shù)本身不是一個簡單的操作,它的時間復雜度是 T2(n) = O(n),所以,整個 cal() 函數(shù)的時間復雜度就是:
我剛剛講了三種復雜度的分析技巧。不過,你并不用刻意去記憶。實際上,復雜度分析這個東西關(guān)鍵在于“熟練”。你只要多看案例,多分析,就能做到“無招勝有招”。幾種常見時間復雜度實例分析
幾種常見時間復雜度實例分析
O(1)
先你必須明確一個概念,O(1) 只是常量級時間復雜度的一種表示方法,并不是指只執(zhí)行了一行代碼。比如這段代碼,即便有 3 行,它的時間復雜度也是 O(1),而不是 O(3)。
int i = 8;int j = 6;int sum = i + j;我稍微總結(jié)一下,只要代碼的執(zhí)行時間不隨 n 的增大而增長,這樣代碼的時間復雜度我們都記作 O(1)?;蛘哒f,一般情況下,只要算法中不存在循環(huán)語句、遞歸語句,即使有成千上萬行的代碼,其時間復雜度也是Ο(1)。
O(logn)、O(nlogn)
對數(shù)階時間復雜度非常常見,同時也是最難分析的一種時間復雜度。我通過一個例子來說明一下。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-h4AgG8p1-1598837281959)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200829171510100.png)]
根據(jù)我們前面講的復雜度分析方法,第三行代碼是循環(huán)執(zhí)行次數(shù)最多的。所以,我們只要能計算出這行代碼被執(zhí)行了多少次,就能知道整段代碼的時間復雜度。從代碼中可以看出,變量 i 的值從 1 開始取,每循環(huán)一次就乘以 2。當大于 n 時,循環(huán)結(jié)束。還記得我們高中學過的等比數(shù)列嗎?實際上,變量 i 的取值就是一個等比數(shù)列。如果我把它一個一個列出來,就應該是這個樣子的:
所以,我們只要知道 x 值是多少,就知道這行代碼執(zhí)行的次數(shù)了。通過 2x=n 求解 x 這個問題我們想高中應該就學過了,我就不多說了。x=log2n,所以,這段代碼的時間復雜度就是 O(log2n)。
現(xiàn)在,我把代碼稍微改下,你再看看,這段代碼的時間復雜度是多少?
根據(jù)我剛剛講的思路,很簡單就能看出來,這段代碼的時間復雜度為 O(log3n)。
實際上,不管是以 2 為底、以 3 為底,還是以 10 為底,我們可以把所有對數(shù)階的時間復雜度都記為 O(logn)。
為什么呢?我們知道,對數(shù)之間是可以互相轉(zhuǎn)換的,log3n 就等于 log32 * log2n,所以 O(log3n) = O(C * log2n),其中 C=log32 是一個常量?;谖覀兦懊娴囊粋€理論:在采用大 O 標記復雜度的時候,可以忽略系數(shù),即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等于 O(log3n)。因此,在對數(shù)階時間復雜度的表示方法里,我們忽略對數(shù)的“底”,統(tǒng)一表示為 O(logn)。
如果你理解了我前面講的 O(logn),那 O(nlogn) 就很容易理解了。還記得我們剛講的乘法法則嗎?如果一段代碼的時間復雜度是 O(logn),我們循環(huán)執(zhí)行 n 遍,時間復雜度就是 O(nlogn) 了。而且,O(nlogn) 也是一種非常常見的算法時間復雜度。比如,歸并排序、快速排序的時間復雜度都是 O(nlogn)。
O(m+n)、O(m*n)
我們再來講一種跟前面都不一樣的時間復雜度,代碼的復雜度由兩個數(shù)據(jù)的規(guī)模來決定。老規(guī)矩,先看代碼!
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 是表示兩個數(shù)據(jù)規(guī)模。我們無法事先評估 m 和 n 誰的量級大,所以我們在表示復雜度的時候,就不能簡單地利用加法法則,省略掉其中一個。
所以,上面代碼的時間復雜度就是 O(m+n)。針對這種情況,原來的加法法則就不正確了,我們需要將加法規(guī)則改為:T1(m) + T2(n) = O(f(m) + g(n))。但是乘法法則繼續(xù)有效:T1(m)*T2(n) = O(f(m) * f(n))。
淺析最好、最壞、平均、均攤時間復雜度
最好、最壞情況時間復雜度
// n表示數(shù)組array的長度 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; }你應該可以看出來,這段代碼要實現(xiàn)的功能是,在一個無序的數(shù)組(array)中,查找變量 x 出現(xiàn)的位置。如果沒有找到,就返回 -1。按照上節(jié)課講的分析方法,這段代碼的復雜度是 O(n),其中,n 代表數(shù)組的長度。
我們在數(shù)組中查找一個數(shù)據(jù),并不需要每次都把整個數(shù)組都遍歷一遍,因為有可能中途找到就可以提前結(jié)束循環(huán)了。但是,這段代碼寫得不夠高效。我們可以這樣優(yōu)化一下這段查找代碼。
// n表示數(shù)組array的長度 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; }這個時候,問題就來了。我們優(yōu)化完之后,這段代碼的時間復雜度還是 O(n) 嗎?
很顯然,咱們上一節(jié)講的分析方法,解決不了這個問題。因為,要查找的變量 x 可能出現(xiàn)在數(shù)組的任意位置。如果數(shù)組中第一個元素正好是要查找的變量 x,那就不需要繼續(xù)遍歷剩下的 n-1 個數(shù)據(jù)了,那時間復雜度就是 O(1)。但如果數(shù)組中不存在變量 x,那我們就需要把整個數(shù)組都遍歷一遍,時間復雜度就成了 O(n)。
所以,不同的情況下,這段代碼的時間復雜度是不一樣的。為了表示代碼在不同情況下的不同時間復雜度,我們需要引入三個概念:最好情況時間復雜度、最壞情況時間復雜度和平均情況時間復雜度。
顧名思義,最好情況時間復雜度就是,在最理想的情況下,執(zhí)行這段代碼的時間復雜度。就像我們剛剛講到的,在最理想的情況下,要查找的變量 x 正好是數(shù)組的第一個元素,這個時候?qū)臅r間復雜度就是最好情況時間復雜度。
同理,最壞情況時間復雜度就是,在最糟糕的情況下,執(zhí)行這段代碼的時間復雜度。就像剛舉的那個例子,如果數(shù)組中沒有要查找的變量 x,我們需要把整個數(shù)組都遍歷一遍才行,所以這種最糟糕情況下對應的時間復雜度就是最壞情況時間復雜度。
具體的平均情況時間復雜度和均攤時間復雜度就記錄了,有興趣的可以去看看。
總結(jié)
一、什么是復雜度分析?
1.數(shù)據(jù)結(jié)構(gòu)和算法解決是“如何讓計算機更快時間、更省空間的解決問題”。
2.因此需從執(zhí)行時間和占用空間兩個維度來評估數(shù)據(jù)結(jié)構(gòu)和算法的性能。
3.分別用時間復雜度和空間復雜度兩個概念來描述性能問題,二者統(tǒng)稱為復雜度。
4.復雜度描述的是算法執(zhí)行時間(或占用空間)與數(shù)據(jù)規(guī)模的增長關(guān)系。
二、為什么要進行復雜度分析?
1.和性能測試相比,復雜度分析有不依賴執(zhí)行環(huán)境、成本低、效率高、易操作、指導性強的特點。
2.掌握復雜度分析,將能編寫出性能更優(yōu)的代碼,有利于降低系統(tǒng)開發(fā)和維護成本。
三、如何進行復雜度分析?
1.大O表示法
1)來源
算法的執(zhí)行時間與每行代碼的執(zhí)行次數(shù)成正比,用T(n) = O(f(n))表示,其中T(n)表示算法執(zhí)行總時間,f(n)表示每行代碼執(zhí)行總次數(shù),而n往往表示數(shù)據(jù)的規(guī)模。
2)特點
以時間復雜度為例,由于時間復雜度描述的是算法執(zhí)行時間與數(shù)據(jù)規(guī)模的增長變化趨勢,所以常量階、低階以及系數(shù)實際上對這種增長趨勢不產(chǎn)決定性影響,所以在做時間復雜度分析時忽略這些項。
2.復雜度分析法則
1)單段代碼看高頻:比如循環(huán)。
2)多段代碼取最大:比如一段代碼中有單循環(huán)和多重循環(huán),那么取多重循環(huán)的復雜度。
3)嵌套代碼求乘積:比如遞歸、多重循環(huán)等
4)多個規(guī)模求加法:比如方法有兩個參數(shù)控制兩個循環(huán)的次數(shù),那么這時就取二者復雜度相加。
四、常用的復雜度級別?
多項式階:隨著數(shù)據(jù)規(guī)模的增長,算法的執(zhí)行時間和空間占用,按照多項式的比例增長。包括,
O(1)(常數(shù)階)、O(logn)(對數(shù)階)、O(n)(線性階)、O(nlogn)(線性對數(shù)階)、O(n2)(平方階)、O(n3)(立方階)
非多項式階:隨著數(shù)據(jù)規(guī)模的增長,算法的執(zhí)行時間和空間占用暴增,這類算法性能極差。包括,
O(2^n)(指數(shù)階)、O(n!)(階乘階)
五、如何掌握好復雜度分析方法?
復雜度分析關(guān)鍵在于多練,所謂孰能生巧。
六、復雜度分析的4個概念
1.最壞情況時間復雜度:代碼在最理想情況下執(zhí)行的時間復雜度。
2.最好情況時間復雜度:代碼在最壞情況下執(zhí)行的時間復雜度。
3.平均時間復雜度:用代碼在所有情況下執(zhí)行的次數(shù)的加權(quán)平均值表示。
4.均攤時間復雜度:在代碼執(zhí)行的所有復雜度情況中絕大部分是低級別的復雜度,個別情況是高級別復雜度且發(fā)生具有時序關(guān)系時,可以將個別高級別復雜度均攤到低級別復雜度上。基本上均攤結(jié)果就等于低級別復雜度。
七、為什么要引入這4個概念?
1.同一段代碼在不同情況下時間復雜度會出現(xiàn)量級差異,為了更全面,更準確的描述代碼的時間復雜度,所以引入這4個概念。
2.代碼復雜度在不同情況下出現(xiàn)量級差別時才需要區(qū)別這四種復雜度。大多數(shù)情況下,是不需要區(qū)別分析它們的。
八、如何分析平均、均攤時間復雜度?
1.平均時間復雜度
代碼在不同情況下復雜度出現(xiàn)量級差別,則用代碼所有可能情況下執(zhí)行次數(shù)的加權(quán)平均值表示。
2.均攤時間復雜度
兩個條件滿足時使用:1)代碼在絕大多數(shù)情況下是低級別復雜度,只有極少數(shù)情況是高級別復雜度;2)低級別和高級別復雜度出現(xiàn)具有時序規(guī)律。均攤結(jié)果一般都等于低級別復雜度。
情況下時間復雜度會出現(xiàn)量級差異,為了更全面,更準確的描述代碼的時間復雜度,所以引入這4個概念。
2.代碼復雜度在不同情況下出現(xiàn)量級差別時才需要區(qū)別這四種復雜度。大多數(shù)情況下,是不需要區(qū)別分析它們的。
八、如何分析平均、均攤時間復雜度?
1.平均時間復雜度
代碼在不同情況下復雜度出現(xiàn)量級差別,則用代碼所有可能情況下執(zhí)行次數(shù)的加權(quán)平均值表示。
2.均攤時間復雜度
兩個條件滿足時使用:1)代碼在絕大多數(shù)情況下是低級別復雜度,只有極少數(shù)情況是高級別復雜度;2)低級別和高級別復雜度出現(xiàn)具有時序規(guī)律。均攤結(jié)果一般都等于低級別復雜度。
總結(jié)均為極客時間用戶“姜威”的評論總結(jié)。
總結(jié)
以上是生活随笔為你收集整理的复杂度分析(大O表示法)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 非线性降维方法 Isomap Embed
- 下一篇: ubuntu20.04耳机没有声音