資料結構和演算法本身解決的是“快”和“省”的問題,即如何讓程式碼執行的更快,如何讓程式碼更省儲存空間。 所以複雜度分析是整個演算法學習的精髓。
大O複雜度表示法
推倒大O階
演算法的執行效率,就是演算法程式碼執行的時間。來估算一下下面程式碼的執行時間。
function sum(n) {
let total = 0;
for (let i=0;i < n;i++) {
total = total + i
}
return total;
}
複製程式碼
假設每行程式碼執行的時間都一樣,都是unit_time。
第 2 行程式碼需要 1 個unit_time 的執行時間,第 3、4 行都執行了 n 遍,所以需要 2n*unit_time 的執行時間,所以這段程式碼總的執行時間就是 (2n+1)*unit_time。
可以看出,程式碼的執行時間T(n) 與每行程式碼的執行次數成正比
再來推導下面程式碼的執行時間:
function sum(n) {
let total = 0;
for (let i=1;i<=n;i++) {
for (let j=1;j<=n;j++) {
total = total + i * j
}
}
return total
}
複製程式碼
同樣第2行需要 1個 unit_time 的執行時間,第3行程式碼迴圈執行了n遍,需要n * unit_time的執行時間,第4、5行執行了n² 遍,所以需要(2n²+n+1)*unit_time 的執行時間。
儘管我們不知道unit_time的具體值,但通過兩段程式碼的執行時間推導過程,我們得到一個非常重要的規律,那就是,所有程式碼的執行時間T(n) 與每行程式碼的執行次數n成正比。
T(n) = O(f(n))
複製程式碼
T(n) 代表程式碼執行的時間,n表示資料規模的大小;f(n)表示每行程式碼執行的次數總和。公式中的O,表示程式碼的執行時間T(n) 與f(n) 表示式成正比。
大O時間複雜度實際上並不具體表示程式碼真正的執行時間,而是表示***程式碼執行時間隨資料規模增加變化的趨勢***,所以,也叫做漸進時間複雜度。
當n很大時,公式中的低階、常量、係數三部分並不左右增長的趨勢,所以可以忽略。我們只需要記錄一個最大量級就可以了,所以上面兩段程式碼就可以計作:T(n) = O(n) 和 T(n) = O(n²)。
時間複雜度分析方法
- 只關注迴圈次數最多的一段程式碼
- 加法法則:總複雜度等於量級最大的那段程式碼的複雜度
- 乘法法則:巢狀程式碼的複雜度等於巢狀內外程式碼複雜度的乘積
常見的時間複雜度分析
常數階 O(1)
O(1)只是常量級時間複雜度的一種表示方法,並不是指只執行了一行程式碼,比如下面這段程式碼,即使有3行,他的時間複雜度也是O(1),而不是O(3)。
let i = 2;
let j = 5;
let sun = i + j;
複製程式碼
一般情況下,只要演算法中不存在迴圈語句、遞迴語句,即便有成千上萬行程式碼,其時間複雜度也是O(1)。
線性階 O(n)
下面這段程式碼的時間複雜度為O(n),因為迴圈體中的程式碼需要執行n次。
let sum = 0;
for (let i=0;i<n;i++){
sum = sum +i
}
複製程式碼
我們要分析演算法的複雜度,關鍵就是要分析迴圈結構的執行情況。上面程式碼因為迴圈體中程式碼需要執行n次,所以時間複雜度為O(n)。
對數階 O(logn) O(nlogn)
對數階時間複雜度非常常見。
let i = 1;
while (i <= n) {
i = i * 2
}
複製程式碼
從上面程式碼中可用看出,遍歷從1開始,每迴圈一次就乘以2,當i大於n時迴圈結束。由於2的x次方等於n,x = log2n,這個迴圈的時間複雜度就是O(log2n)。不管是以2為底,還是以3為底,我們都可以把所有對數階的時間複雜度計作:O(logn)。
平方階 O(n²)
for (let i=0;i<n;i++) {
for (let j=o;j<n;j++) {
/*時間複雜度為O(1)的程式步驟*/
}
}
複製程式碼
根據迴圈的時間複雜度等於迴圈體的複雜度乘以該迴圈執行的次數。所以上面程式碼的時間複雜度為O(n²)。
最優演算法
一般情況下,隨著n的增大,T(n) 增長最慢的演算法為最優演算法。
最壞情況
我們查詢一個有n個隨機數字陣列中的某個數字,最好的情況是陣列的第一個數字就是,那麼演算法的時間複雜度為O(1),但也可能這個數字在最後一位,那麼演算法的時間複雜度就是O(n),這就是最壞情況了。
最壞情況執行時間是一種保證,那就是執行時間不會再壞了。通常除非特別指定,我們提到的執行時間都是最壞情況的執行時間。
本文摘自極客時間王爭老師的 資料結構與演算法之美
摘自《大話資料結構》