數據結構和算法本身解決的是 ‘快’ 和 '省 ’ 的問題,所以執行效率是算法一個重要的考量標準,如何衡量編寫的算法的執行效率,就要用到所謂的大O表示法。
大 O 複雜度表示法
算法的執行效率,粗略地講,就是算法代碼執行的時間。但是,如何在不運行代碼的情況下,用“肉眼”得到一段代碼的執行時間呢?
這裏有段非常簡單的代碼,求 1,2,3…n 的累加和。估算一下這段代碼的執行時間
int cal(int n) {
int sum = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum = sum + i * j;
}
}
}
假設每一行代碼的執行時間爲一個時間單位
第 2、3、4 行代碼,每行都需要 1 個 unit_time 的執行時間,第 5、6 行代碼循環執行了 n 遍,需要 2n * unit_time 的執行時間,第 7、8 行代碼循環執行了 n²遍,所以需要 2n² * unit_time 的執行時間。所以,整段代碼總的執行時間 T(n) = (2n²+2n+3)*unit_time
儘管我們不知道 unit_time 的具體值,但是通過這兩段代碼執行時間的推導過程,我們可以得到一個非常重要的規律,那就是,所有代碼的執行時間 T(n) 與每行代碼的執行次數 n 成正比
我們可以把這個規律總結成一個公式,那麼就形成大O表示法
其中,T(n) ,它表示代碼執行的時間;n 表示數據規模的大小;f(n) 表示每行代碼執行的次數總和。因爲這是一個公式,所以用 f(n) 來表示。公式中的 O,表示代碼的執行時間 T(n) 與 f(n) 表達式成正比。
所以,第一個例子中的 T(n) = O(2n²+2n+3)。這就是大 O 時間複雜度表示法。大 O 時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。
當 n 很大時,你可以把它想象成 10000、100000。而公式中的低階、常量、係數三部分並不左右增長趨勢,所以都可以忽略。我們只需要記錄一個最大量級就可以了,如果用大 O 表示法表示剛講的那兩段代碼的時間複雜度,就可以記爲: T(n) = O(n²)。
時間複雜度分析
- 只關注循環執行次數最多的一段代碼
大 O 這種複雜度表示方法只是表示一種變化趨勢。我們通常會忽略掉公式中的常量、低階、係數,只需要記錄一個最大階的量級就可以了。所以,我們在分析一個算法、一段代碼的時間複雜度的時候,也只關注循環執行次數最多的那一段代碼就可以了。這段核心代碼執行次數的 n 的量級,就是整段要分析代碼的時間複雜度
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}
如這段代碼的時間複雜度即爲O(n)
- 加法法則:總複雜度等於量級最大的那段代碼的複雜度
int cal(int n) {
int sum_1 = 0;
int p = 1;
for (; p < 100; ++p) {
sum_1 = sum_1 + p;
}
int sum_2 = 0;
int q = 1;
for (; q < n; ++q) {
sum_2 = sum_2 + q;
}
int sum_3 = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum_3 = sum_3 + i * j;
}
}
時間複雜度爲O(n²) 取三個中最大的那個 第一段的時間複雜度爲常量時間與n無關
強調一個 即便這段代碼循環 10000 次、100000 次,只要是一個已知的數,跟 n 無關,照樣也是常量級的執行時間。當 n 無限大的時候,就可以忽略。儘管對代碼的執行時間會有很大影響,但是回到時間複雜度的概念來說,它表示的是一個算法執行效率與數據規模增長的變化趨勢,所以不管常量的執行時間多大,我們都可以忽略掉。因爲它本身對增長趨勢並沒有影響。
- 乘法法則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積
int cal(int n) {
int ret = 0;
int i = 1;
for (; i < n; ++i) {
ret = ret + f(i);
}
}
int f(int n) {
int sum = 0;
int i = 1;
for (; i < n; ++i) {
sum = sum + i;
}
return sum;
}
時間複雜度爲O(n²)
幾種常見時間複雜度實例分析
- O(1)
首先你必須明確一個概念,O(1) 只是常量級時間複雜度的一種表示方法,並不是指只執行了一行代碼。比如這段代碼,即便有 3 行,它的時間複雜度也是 O(1),而不是 O(3)。
int i = 8;
int j = 6;
int sum = i + j;
只要代碼的執行時間不隨 n 的增大而增長,這樣代碼的時間複雜度我們都記作 O(1)。或者說,一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬行的代碼,其時間複雜度也是Ο(1)。
- O(logn)、O(nlogn)
i=1;
while (i <= n) {
i = i * 2;
}
在對數階時間複雜度的表示方法裏,我們忽略對數的“底”,統一表示爲 O(logn)。
- O(m+n)、O(m*n)
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 是表示兩個數據規模。我們無法事先評估 m 和 n 誰的量級大,所以我們在表示複雜度的時候,就不能簡單地利用加法法則,省略掉其中一個。所以,上面代碼的時間複雜度就是 O(m+n)。