一、複雜度分析
首先要明確一點,數據結構和算法本質是解決“快”和“省”的問題。要描述一個算法的好壞就需要用到複雜度分析了,複雜度分析可分爲如下兩種。
-
時間複雜度
-
空間複雜度
時間複雜度就是描述算法的快,空間複雜度則是描述算法的省。一般說的複雜度都是時間複雜度,畢竟現代計算機存儲空間已經不那麼拮据了,時間複雜度是我們重點研究的內容。
二、大 O
複雜度表示法
首先看一段代碼,求從 1~n
的累加之和。
int demo(int n) {
int i;
int sum = 0;
for(i=1; i<n; i++) {
sum += i;
}
return sum;
}
現在就來估算一下這段代碼的執行時間(下面都是以時間複雜度爲例講解,空間複雜度最後再講)。
從 CPU
的角度來看,每一行代碼都執行着類似的操作讀數據-運算-寫數據。這裏爲了方便計算,假設每行代碼的執行時間都是一樣的,用 t
表示執行一行代碼所需要的時間,n
表示數據規模的大小,T(n)
表示代碼執行的總時間。
那麼這段代碼總執行時間是多少呢?我們來數一下。
首先,函數體內有 5
條語句,第 1、2、5
條語句總共執行了 3
次,所需時間是 3*t
;第 3、4
條語句各自執行了 n
次,所需時間是 2*n*t
。把這兩個代碼段執行的時間相加,所得到的結果就是這段代碼總共所需的時間。
通過上述公式可以得到一個規律,T(n)
隨着 n
變大而變大,變小而變小。所以,T(n)
與 n
是成正比的,用數學符號表示就可以寫成。
其中 f(n)
是代碼段執行所需的時間之和,O
表示 T(n)
與 f(n)
之間的關係是成正比的。
由公式可得代碼段執行所需的時間可表示爲 。這就是大 O
時間複雜度表示法。大 O
時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨着數據規模增長的變化趨勢,所以,也叫做漸進時間複雜度簡稱時間複雜度。
其實並不是最終時間複雜度的表示方式。在實際的複雜度分析中,一般會把公式中的常量、係數、低階忽略。因爲這三部分並不影響增長趨勢(還記得時間複雜度其實是漸進時間複雜度吧!),所以只需要記錄一個最大量級就可以了,時間複雜度的最終表示方式就是。
三、複雜度的分析方法
1. 最大量階
int demo(int n) {
int i;
int sum = 0;
for(i=1; i<n; i++) {
sum += i;
}
return sum;
}
在分析一個算法、一段代碼的時間複雜度的時候,只關注循環執行次數最多的那一段代碼即可。
2. 加法法則
int demo(int n) {
int i;
int sum = 0;
for(i=1; i<n; i++) {
sum += i;
}
for(i=1; i<n; i++) {
int j;
for (j=1; j<n; j++)
sum += i;
}
return sum;
}
如果代碼中存在着不同量級的時間複雜度,總的時間複雜度就等於量級最大的那段代碼的時間複雜度。
3. 乘法法則
int demo(int n) {
int i;
int sum = 0;
for(i=1; i<n; i++) {
int j;
for (j=1; j<n; j++)
sum += i;
}
return sum;
}
如果是嵌套、函數調用、遞歸等操作,只需要將各部分相乘即可。
四、複雜度的量級
-
常量階:
-
對數階:
-
線性階:
-
線性對數階:
-
平方階:
-
立方階:
-
k
次方階: -
指數階:
-
階乘階:
對於上述不同的量級可以分爲兩類:多項式量級和非多項式量級。其中,非多項式量級只有兩個:和,非多項式也叫做 NP
問題。
一般情況下,我們常見的複雜度只有、、、、 這五個,常用的分析方法有最大量階、加法法則、乘法法則這三個。只要把這些掌握,基本上就沒有太大問題了。
五、時間複雜度
我們已經分析了時間複雜度,但是還是有一點兒小問題,比如我們要查找某個元素在長度爲 n
的數組中的下標。如果按照順序遍歷,最理想的情況是第一個就是我們要找的,所以時間複雜度是 O(1)
;如果最後一個才找到我們要的數據,那麼它的時間複雜度是 O(n)
。
爲了解決同一段代碼在不同情況下時間複雜度出現量級差異,我們就需要對時間複雜度進一步細化分類,爲了更準確、更全面的描述代碼的時間複雜度,引入了一下 4
個概念。
1. 最好情況時間複雜度
代碼在最理想情況下執行的時間複雜度。
2. 最壞情況時間複雜度
代碼在最壞情況下執行的時間複雜度。
3. 平均情況時間複雜度
上面兩個最好、最壞情況都是小概率事件,平均情況時間複雜度纔是最能代表一個算法的時間複雜度。因爲平均情況時間複雜度需要引入概率進行分析,所以也叫做加權平均時間複雜度。
4. 均攤時間複雜度
正常情況下,代碼在執行過程中都處於低階的複雜度,極個別情況會出現高階的複雜度,這是我們就可以將高階的複雜度均攤到每個低階的複雜度上,這種分析使用的是攤還分析法的思想。
其實我們只需要知道時間複雜度就夠了。這四種方法都是對時間複雜度的一些特殊情況的補充,也沒必要花大力氣去研究它,大概知道有這種時間複雜度分類就可以了,如果你自己想學或者有腦殘面試官要問這些,那你就自己去查找資料研究研究,這裏不會展開講解。
六、空間複雜度
前面講解過,時間複雜度是漸進時間複雜度,表示算法的執行時間與數據規模之間的增長關係。那麼空間複雜度就是漸進空間複雜度,表示算法的存儲空間與數據規模之間的增長關係。
看一段代碼,定義一個新數組,賦值後遍歷輸出。
void demo(int n) {
int i;
int data[n];
for(i=0; i<n; i++) {
data[i] = i * i;
}
for(i=0; i<n; i++) {
printf("%d\n", data[i]);
}
}
跟時間複雜度分析一樣,函數體內第 1
條語句是常量階,直接忽略;第 2
條語句申請了一個大小爲 n
的 int
類型數組,所以整段代碼的空間複雜度就是。