復雜度也稱為漸進復雜度,包括漸進時間復雜度和漸進空間復雜度,描述算法隨數據規模變化而逐漸變化的趨勢。復雜度分析是評估算法好壞的基礎理論方法,所以掌握好復雜度分析方法是很有必要的。
時間復雜度
首先,學習數據結構是為了解決“快”和“省”的問題,那么如何去評估算法的速度快和省空間呢?這就需要掌握時間和空間復雜度分析。同一段代碼運行在不同環境、不同配置機器、處理不同量級數據…效率肯定不會相同。時間復雜度和空間復雜度是不運行代碼,從理論上粗略估計算法執行效率的方法。時間復雜度一般用O來表示,如下例子:計算1,2,3…n的和。CPU執行每行代碼時間很快,假設每行執行時間都一樣為unit_time,第2行為一個unit_time,第3、4行都執行了n遍,那么下面這段代碼執行的耗時時間可以這么計算:(1+2*n) * unit_time。
1 public int sum(int n) { 2 int sum = 0; 3 for (int i = 1; i <= n; i++) { 4 sum = sum + i; 5 } 6 return sum; 7 }
類似的再看一個例子:
1 public int sum(int n) { 2 int sum = 0; 3 int i = 1; 4 int j; 5 for (; i <= n; i++) { 6 j = 1; 7 for (; j <= n; j++) { 8 sum = sum + i * j; 9 } 10 } 11 return sum; 12 }
第2、3、4行分別執行執行了一次,時間為3unit_time,第5、6兩行循環了n次為2n * unit_time,第7、8兩行執行了n*n次為(n²) * unit_time,所以總的執行時間為:(2n²+2n+3) * unit_time
可以看出來,所有代碼執行時間T(n)與每行代碼執行次數成正比。可以用如下公式來表示:
T(n) = O(f(n))
T(n)表示代碼的執行時間;
n表示數據規模大小;
f(n)表示每行代碼執行的次數和,是一個表達式;
O表示執行時間T(n)和f(n)表達式成正比
那么上面兩個時間復雜度可以表示為:
T(n) = O(1+2*n) 和 T(n) = O(2n²+2n+3)
實際上O並不表示具體的執行時間,只是表示代碼執行時間隨數據規模變化的趨勢,所以時間復雜度實際上是漸進時間復雜度的簡稱。當n很大時,系數對結果的影響很小可以忽略,上面兩個例子的時間復雜度可以粗略簡化為:
T(n) = O(n) 和 T(n) = O(n²)
因為時間復雜度是表示的一種趨勢,所以常常忽略常量、低階、系數,只需要最大階量級就可以了。
分析時間復雜度的幾個常見法則
1、只關注代碼執行最多的一段代碼
上面例子可以看出,復雜度忽略了低階、常量和系數,所以執行最多的那一段最能表達時間復雜度的趨勢。
2、加法法則:總復雜度等於各部分求和,然后取復雜度量級最高的
還是上面的例子,總的時間復雜度等於各部分代碼時間復雜度的和,求和之后再用最能表達趨勢的項來表示整段代碼的時間復雜度。
3、乘法法則:嵌套代碼復雜度等於嵌套內外代碼復雜度的乘積
上面第二段代碼,j 循環段嵌套在 i 循環內部,所以 j 循環體內的時間復雜度等於單獨 i 的時間復雜度乘以單獨 j 的時間復雜度。
常見的時間復雜度表示
常見的復雜度有以下幾種
- 常量階:O(1)
- 對數階:O(logn)
- 線性階:O(n)
- 線性對數階:O(nlogn)
- 平方階:O(n²)、立方階O(n³)……
- 指數階:O(2ⁿ)
- 階乘階:O(n!)
可以這么來理解:如果一段代碼有1000或10000行甚至更多,行數是一個常量,不會隨着數據規模增大而變化,我們就認為時間復雜度為一個常量,用O(1)表示。
這幾種復雜度效率曲線比較
模擬一個數組動態擴容例子,如果數組長度夠,直接往里面插入一條數據;反之,將數組擴充一倍,然后往里面插入一條數據:
1 int[] arr = new int[10]; 2 int len = arr.length; 3 int i = 0; 4 public void add(int item) { 5 if (i >= len) { 6 int[] new_arr = new int[len * 2]; 7 for (int i = 0; i < len; i++) { 8 new_arr[i] = arr[i]; 9 } 10 arr = new_arr; 11 len = arr.length; 12 } 13 arr[i] = item; 14 i++; 15 }
最好時間復雜度(best case time complexity)
最好情況下某個算法的時間復雜度。最好情況下,數組空間足夠,只需要執行插入數據就可以了,此時時間復雜度是O(1)。
最壞時間復雜度(worst case time complexity)
最壞情況下某個算法的時間復雜度。最壞情況下數組滿了,需要先申請一個空間為原來兩倍的數組,然后將數據拷貝進去,此時時間復雜度為O(n)。一般情況下我們說算法復雜度就是指的最壞情況時間復雜度,因為算法時間復雜度不會比最壞情況復雜度更差了。
平均時間復雜度(average case time complexity)
最好時間復雜度和最壞時間復雜度都是極端情況下的時間復雜度,發生的概率並不算很大。平均時間復雜度是描述各種情況下平均的時間復雜度。上面的動態擴容例子將1到n+1次為一組來分析,前面n次的時間復雜度都是1,第n+1次時間復雜度是n,將一個數插入數組里的1 至 (n+1)個位置概率都為1/(n+1),所以平均時間復雜度為:
O(n) = (1 + 1 + 1 + …+n)/(n+1) = O(1)
均攤時間復雜度(amortized time complexity)
對一個數據結構進行一組連續的操作中,大部分情況下時間復雜度都很低,只有個別情況下時間復雜度比較高,而且這些操作之間存在前后連續的關系。並且和這組數據類型的情況循環往復出現,這時候可以將這一組數據作為一個整體來分析,看看是否可以將最后一個耗時的操作復雜度均攤到其他的操作上,如果可以,那么這種分析方法就是均攤時間復雜度分析法。上面的例子來講,第n+1次插入數據時候,數組剛好發生擴容,時間復雜度為O(n),前面n次剛好將數組填滿,每次時間復雜度都為O(1),此時可以將第n+1次均攤到前面的n次上去,所以總的均攤時間復雜度還是O(1)。
空間復雜度
類比時間復雜度,如下代碼所示,第2行申請了一個長度為n的數據,第三行申請一個變量i為常量可以忽略,所以空間復雜度為O(n)
1 public void init(int n) { 2 int[] arr = new int[n]; 3 int i = 0; 4 for (; i < n; i++) { 5 arr[i] = i + 1; 6 } 7 }
一般情況下,一個程序在機器上執行時,除了需要存儲程序本身的指令、常數、變量和輸入數據外,還需要存儲對數據操作的存儲單元,若輸入數據所占空間只取決於問題本身,和算法無關,這樣只需要分析該算法在實現時所需的輔助單元即可。若算法執行時所需的輔助空間相對於輸入數據量而言是個常數,則稱此算法為原地工作,空間復雜度為O(1)。