本章內(nèi)容源于筆者對極客時間《數(shù)據(jù)結(jié)構(gòu)與算法之美》以下章節(jié)的學(xué)習(xí)筆記:
復(fù)雜度分析是整個算法學(xué)習(xí)的精髓,只要掌握了它,數(shù)據(jù)結(jié)構(gòu)和算法的內(nèi)容基本上就掌握了一半。
為什么需要復(fù)雜度分析
把代碼跑一遍,通過統(tǒng)計、監(jiān)控得到算法執(zhí)行的時間和占用的內(nèi)存大小,這種方法稱為事后統(tǒng)計法,有非常大的局限性:
- 1.測試結(jié)果非常依賴測試環(huán)境
- 2.測試結(jié)果受數(shù)據(jù)規(guī)模的影響很大
我們需要一個不用關(guān)心的宿主環(huán)境、不用具體的測試數(shù)據(jù)來測試,就可以粗略地估計算法的執(zhí)行效率的方法。就是時間、空間復(fù)雜度分析法。
大O復(fù)雜度表示法
假設(shè)每行代碼執(zhí)行的時間都一樣,為單位時間,所有代碼的執(zhí)行時間T(n)與每行代碼的執(zhí)行次數(shù)成正比。我們把這個規(guī)律總結(jié)成一個公式:T(n) = O(f(n))。
- T(n)表示代碼執(zhí)行的時間
- f(n)表示每行代碼執(zhí)行的次數(shù)總和
- n表示數(shù)據(jù)規(guī)模的大小
這就是大O時間復(fù)雜度表示法。
- 注意:大O時間復(fù)雜度實際上并不代表真正的執(zhí)行時間,表示代碼執(zhí)行時間隨數(shù)據(jù)規(guī)模增長的變化趨勢。公式中的低階、常量、系數(shù)三部分并不左右增長趨勢,所以忽略。
時間復(fù)雜度分析
時間復(fù)雜度全稱漸進時間復(fù)雜度。分析技巧如下:
- 1.只關(guān)注循環(huán)執(zhí)行次數(shù)最多的一段代碼
- 2.加法法則:總復(fù)雜度等于量級最大的那段代碼的復(fù)雜度
- 3.乘法法則:嵌套代碼的復(fù)雜度等于嵌套內(nèi)外代碼復(fù)雜度的乘積
強調(diào)一下,即便一段段代碼循環(huán)10000次、100000次,只要是一個已知的數(shù),跟n無關(guān),照樣也是常量級的執(zhí)行時間。當n無限大的時候,就可以忽略。
以上三種復(fù)雜度的分析技巧不用刻意去記憶,實際上,復(fù)雜度分析這個東西關(guān)鍵在于“熟練”,多看案例多分析。
幾種常見時間復(fù)雜度實例分析
常見復(fù)雜度量級并不多,以下幾乎涵蓋了今后接觸的所有代碼的復(fù)雜度量級。
復(fù)雜度量級 | 大O表達式 | 所屬分類 |
---|---|---|
常量階 | O(1) | 多項式量級 |
對數(shù)階 | O(logn) | 多項式量級 |
線性階 | O(n) | 多項式量級 |
線性對數(shù)階 | O(nlogn) | 多項式量級 |
平方階 | O(n2) | 多項式量級 |
立方階 | O(n3) | 多項式量級 |
k次方階 | O(nk) | 多項式量級 |
指數(shù)階 | O(2n) | 非多項式量級 |
階乘階 | O(n!) | 非多項式量級 |
O(1)
只要代碼的執(zhí)行時間不隨n的增大而增長,時間復(fù)雜度都記作O(1)。一般情況下,只要算法中不存在循環(huán)語句、遞歸語句,即使有成千上萬行的代碼,其時間復(fù)雜度也是Ο(1)。
O(logn)、O(nlog)
對數(shù)階時間復(fù)雜度非常常見,同時也是最難分析的一種時間復(fù)雜度。
i = 1;
while (i <= n) {
i = i * 2;
}
以上代碼中可以看出,變量i的值從1開始取,每循環(huán)一次就乘以 2,當大于n時,循環(huán)結(jié)束。i的取值是一個等比數(shù)列,求解2x = n,x = log2n。
類似的,log3n = log32 * log2n,不管是以2、3甚至是10為底,都可以轉(zhuǎn)換為c * log2n,忽略系數(shù)和對數(shù)的“底”,都記作O(logn)。
如果一段代碼的時間復(fù)雜度是 O(logn) ,循環(huán)執(zhí)行n遍,時間復(fù)雜度就是 O(nlogn) 了。
O(nlogn) 也是一種非常常見的算法時間復(fù)雜度。比如,歸并排序、快速排序的時間復(fù)雜度都是 O(nlogn)。
O(m+n)、O(m*n)
代碼的復(fù)雜度由兩個數(shù)據(jù)的規(guī)模來決定。
空間復(fù)雜度分析
空間復(fù)雜度全稱漸進空間復(fù)雜度,表示算法的存儲空間與數(shù)據(jù)規(guī)模之間的增長關(guān)系。
空間復(fù)雜度分析比時間復(fù)雜度分析要簡單很多。常見的空間復(fù)雜度就是O(1)、O(n)、O(n2),像 O(logn)、O(nlogn) 這樣的對數(shù)階復(fù)雜度平時都用不到。
小結(jié):復(fù)雜度包括時間復(fù)雜度和空間復(fù)雜度,用來分析算法執(zhí)行效率與數(shù)據(jù)規(guī)模之間的增長關(guān)系。越高階復(fù)雜度的算法執(zhí)行效率越低。常見復(fù)雜度從低階到高階有:
O(1)、O(logn)、O(n)、O(nlogn)、O(n2 )。
復(fù)雜度分析的4個概念
- 最好情況時間復(fù)雜度:代碼在最理想情況下執(zhí)行的時間復(fù)雜度。
- 最壞情況時間復(fù)雜度:代碼在最糟糕情況下執(zhí)行的時間復(fù)雜度。
- 平均情況時間復(fù)雜度:引入概率的概念,代碼在所有情況下執(zhí)行的次數(shù)的加權(quán)平均值。
實際上,在大多數(shù)情況下并不需要區(qū)分最好、最壞、平均情況時間復(fù)雜度。只有同一塊代碼在不同的情況下,時間復(fù)雜度有量級的差距,才會使用這三種復(fù)雜度表示法來區(qū)分。
- 均攤時間復(fù)雜度:利用攤還分析法將個別情況的高復(fù)雜度均攤到大部分情況的低復(fù)雜度所得到的時間復(fù)雜度。
應(yīng)用場景:對一個數(shù)據(jù)結(jié)構(gòu)進行一組連續(xù)操作中,大部分情況下時間復(fù)雜度都很低,只有個別情況下時間復(fù)雜度比較高,而且這些操作之間存在前后連貫的時序關(guān)系,這個時候就可以將這一組操作放在一塊兒分析,看是否能將較高時間復(fù)雜度那次操作的耗時,平攤到其他那些時間復(fù)雜度比較低的操作上。
- 一般均攤時間復(fù)雜度就等于最好情況時間復(fù)雜度。
- 均攤時間復(fù)雜度就是一種特殊的平均時間復(fù)雜度。
小結(jié):引入最好情況時間復(fù)雜度、最壞情況時間復(fù)雜度、平均情況時間復(fù)雜度、均攤時間復(fù)雜度這幾個概念是因為同一段代碼在不同輸入情況下,復(fù)雜度量級有可能不一樣,通過比較分析,我們可以更加全面地表示一段代碼的執(zhí)行效率。
思考題一:有人說,我們項目之前都會進行性能測試,再做代碼的時間復(fù)雜度、空間復(fù)雜度分析,是不是多此一舉呢?而且,每段代碼都分析一下時間復(fù)雜度、空間復(fù)雜度,是不是很浪費時間呢?你怎么看待這個問題呢?
參考回答:
- 1.復(fù)雜度分析是一個理論分析,與宿主無關(guān),能讓程序員在寫代碼時對算法的執(zhí)行效率有個大致認識,從而寫出效率更高的程序;
- 2.通過練習(xí)就能達到熟練地看出是否浪費時間,比如復(fù)雜度越低階效率越高,為代碼質(zhì)量考慮并不算浪費時間。
思考題二:分析下面這個 add()
函數(shù)的時間復(fù)雜度。
// 全局變量,大小為 10 的數(shù)組 array,長度 len,下標 i
int array[] = new int[10];
int len = 10;
int i = 0;
// 往數(shù)組中添加一個元素
void add(int element) {
if (i >= len) { // 數(shù)組空間不夠了
// 重新申請一個2倍大小的數(shù)組空間
int new_array[] = new int[len*2];
// 把原來array數(shù)組中的數(shù)據(jù)依次copy到new_array
for (int j = 0; j < len; ++j) {
new_array[j] = array[j];
}
// new_array 復(fù)制給 array,array 現(xiàn)在大小就是 2 倍 len 了
array = new_array;
len = 2 * len;
}
// 將 element 放到下標為 i 的位置,下標 i 加一
array[i] = element;
++i;
}
參考回答:
以上代碼實現(xiàn)往一個數(shù)組中添加數(shù)據(jù),要是數(shù)組放不下了就將數(shù)組擴大到原來2倍,然后再添加新元素。最壞情況時間復(fù)雜度是 O(n),最好情況時間復(fù)雜度、平均情況時間復(fù)雜度、均攤時間復(fù)雜度都是 O(1)。