算法系列:演算法的時間複雜度(Objective-C樣例)
用這篇部落格記錄一下學習如何計算時間複雜度的過程。本文會從時間複雜度的定義到具體案例的練習,讓初學者對時間複雜度有個基本印象。
摘自《維基百科》
在 ofollow,noindex">電腦科學 中, 演算法 的 時間複雜度 是一個 函式 ,它定性描述該演算法的執行時間。這是一個代表演算法輸入值的 字串 的長度的函式。時間複雜度常用 大O符號 表述,不包括這個函式的低階項和首項係數。使用這種方式時,時間複雜度可被稱為是 漸近 的,亦即考察輸入值大小趨近無窮時的情況。例如,如果一個演算法對於任何大小為 n (必須比 n 0 大)的輸入,它至多需要 5 n 3 + 3 n 的時間執行完畢,那麼它的漸近時間複雜度是 O( n 3 )。
為了計算時間複雜度,我們通常會估計演算法的操作單元數量,每個單元執行的時間都是相同的。因此,總執行時間和演算法的操作單元數量最多相差一個常量係數。
相同大小的不同輸入值仍可能造成演算法的執行時間不同,因此我們通常使用演算法的 最壞情況複雜度 ,記為 T(n) ,定義為任何大小的輸入 n 所需的最大執行時間。
摘自《百度百科》
1.一般情況下,演算法中基本操作重複執行的次數是問題規模n的某個函式,用T(n)表示,若有某個輔助函式f(n),使得T(n)/f(n)的極限值(當n趨近於無窮大時)為不等於零的常數,則稱f(n)是T(n)的同數量級函式。記作T(n)=O(f(n)),稱O(f(n)) 為演算法的漸進時間複雜度,簡稱時間複雜度。
分析:隨著模組n的增大,演算法執行的時間的增長率和 f(n) 的增長率成正比,所以 f(n) 越小,演算法的時間複雜度越低,演算法的效率越高。
2. 在計算時間複雜度的時候,先找出演算法的基本操作,然後根據相應的各語句確定它的執行次數,再找出 T(n) 的同 數量級 (它的同數量級有以下:1,log 2 n,n,n log 2 n ,n的平方,n的三次方,2的n次方,n!),找出後,f(n) = 該數量級,若 T(n)/f(n) 求極限可得到一常數c,則時間複雜度T(n) = O(f(n))
下面是各種常見函式的時間複雜度趨勢圖:

看過了定義概念和趨勢圖之後其實還是不太明白時間複雜度是什麼,所以有必要把時間複雜度再說白一點。挑幾個經典的並且常見的時間複雜度來舉例說明。
我理解的是計算時間複雜度時要本著幾個原則:
- 去掉(忽略)常數項
- 係數是常數的,要把這個係數去掉
- 只留最高次項
比如 T(n) = 4n^3 + 2n^2 + 5n + 10
去掉常數項:T(n) = 4n^3 + 2n^2 + 5n
去掉常數係數:T(n) = n^3 + n^2 + n
只留最高次項:T(n) = n^3
所以最後時間複雜度為 O(n^3)
注: n^3 表示 n 的 3 次冪。
複雜度 O(1) :常數級
例1:
- (void)printHello:(int)n { NSLog(@"Hello world!"); }
方法中只有一句 NSLog
語句,也就是說程式執行這段程式碼的總操作單元數量(總次數)恆等於常數 1。程式碼的執行次數與問題規模 n 無關,這樣的程式碼段時間複雜度就是 O(1) 。
例2:
- (void)conditionalStatement:(int)n { if (n > 10) { NSLog(@"大於 10"); } else { NSLog(@"不大於 10"); } }
方法中先要對引數 n 進行判斷,此處要執行一次。然後如果 n > 10 執行 NSLog(@"大於 10");
,如果 n <= 10 那麼執行 NSLog(@"不大於 10");
。所以無論 n 等於幾,程式碼段執行的總次數恆等於常數 2。或者說最壞的情況(執行次數最多的時候)也是常數 2。所以它的時間複雜度仍然是 O(1) 。
複雜度 O(log n):對數級增長
例3:
- (void)exampleForLogN:(int)n { int num1 = 0, num2 = 0; for(int i=0; i<n; i++){ num1 += 1; for(int j=1; j<=n; j*=2){ num2 += num1; } } }
語句 int num1 = 0, num2 = 0;
的執行次數(頻度)為1;
語句 i=0;
的頻度為1;
語句 i<n;
i++;
num1+=1;
j=1;
的頻度為n;
語句 j<=n;
j*=2;
num2+=num1;
的頻度為 n*log n;
T(n) = 2 + 1 + 4n + 3n*log n,當 n 趨近無窮大時,複雜度會越來越趨近最高冪次項 3n*log n的值,再去掉係數 3,就是我們要算的時間複雜度 O(log n) 。
其中最內層的 num2+=num1;
是執行最多的語句,它的執行次數為什麼是 n*log n 呢?
分析一下:語句 num2+=num1;
處在兩層巢狀的迴圈之中,如果外層迴圈次數為 n ,內層迴圈次數為 m ,那麼語句 num2+=num1;
就會執行 n * m次。程式碼中很容易看出來外層迴圈就是執行 n 次,而內層迴圈到底執行了多少次取決於 j*=2 這句程式碼的趨勢。j 從 1 開始計數,j 的變化趨勢是 j=2 --> j=4 --> j=8 --> j=16... 也就是 j是以指數趨勢增長,假設當 j = n 時執行了 t 次,那麼就有 2^t=n(2 的 t 次冪等於 n),則有次數 t=log n(log 以 2 為底 n 的對數),也就是說問題規模為 n 時,內層迴圈執行了 log n 次。
複雜度 O(n):線性增長
例4:
- (void)exampleForSum:(int)n { int sum = 1; for (int i = 0; i < n; i++) { sum += i; } NSLog(@"%d", sum); }
語句 int sum = 1;
執行 1 次;
語句 int i = 0;
執行 1 次;
語句 i < n;
i++
sum += i;
都會執行 n 次;
語句 NSLog(@"%d", sum);
執行 1 次;
所以 T(n)=3+3n,當 n 趨近無窮大時,有複雜度 T(n) = O(n),即這段程式碼的時間複雜度是 O(n)。
例5:
- (NSInteger)findMaxElement:(NSArray *)array { NSInteger max = [array.firstObject integerValue]; for (int i = 0; i < array.count; i++) { if ([array[i] integerValue] > max) { max = [array[i] integerValue]; } } return max; }
n 為陣列 array 的元素個數,則最壞情況下需要比較 n 次以得到最大值,所以演算法複雜度為 O(n)。
例6:
int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } }
階乘:給定規模 n,演算法基本步驟執行的數量為 n,所以演算法複雜度為 O(n)。
複雜度 O(n^2):乘方級增長
例7:
- (NSInteger)SumMN:(NSInteger)n { NSInteger sum = 0; for (int x = 0; x < n; x++) { for (int y = 0; y < n; y++) { sum += x * y; } } return sum; }
給定規模 n ,則基本步驟的執行數量為 n*n,所以演算法複雜度為 O(n^2)。
例8:
- (NSInteger)findInversions:(NSArray *)array { NSInteger inversions = 0; for (int i = 0; i < array.count; i++) { for (int j = i + 1; j < array.count; j++) { if (array[i] > array[j]) { inversions++; } } } return inversions; }
單位操作if語句在兩層for迴圈中,它的總次數是一個等差數列之和,即首數加尾數乘以個數除以2。
當i=0時,內層執行(n-1)次,當i=n-1時,內層執行 0 次,首數加尾數(n-1+0),乘以個數除以2,T(n) = (n-1)*n/2。
n 為陣列 array 的大小,則基本步驟的執行數量約為 n*(n-1)/2,所以演算法複雜度為 O(n2)。
複雜度 O(2^n):指數級增長
例9 :
- (NSInteger)Calculation:(NSInteger)n { NSInteger result = 0; for (int i = 0; i < (1 << n); i++) { result += i; } return result; }
例子中 i 的迴圈條件是 i < (1 << n),1 << n 表示二進位制的 1 --> 0001 向左移 n 位。比如 n = 1 時向左移 1 位是 0010 ,十進位制就是 2,迴圈體執行 2 次。n = 2 時向左移 2 位是 0100,十進位制就是 4 ,迴圈體執行4次。所以迴圈次數是 2^n,複雜度是 O(2^n)。
例10:
- (NSUInteger)fibonacci:(NSUInteger)n { if (n < 2) { return n; } else { return [self fibonacci:n-1] + [self fibonacci:n-2]; } }
這個是斐波那契數列的遞迴呼叫求解方式。當 n<2 時執行次數是一個常數,即只執行一步返回 n 即可,複雜度為 O(1)。當 n>=2 時,n 會被一層一層拆分,一直拆分為 1 或 0 時才能被確定值。如圖:



通過圖片能看出總執行次數在以指數級增長,所以複雜度是 O(2^n)。
小訓練
測試1:
- (void)smallTest1 { int i = 2; int j = 4; int temp = i; i = j; j = temp; }
以上三條單個語句的頻度為1,該程式段的執行時間是一個與問題規模n無關的常數。
演算法的時間複雜度為常數階,記作T(n)=O(1)。
如果演算法的執行時間不隨著問題規模n的增加而增長,即使演算法中有上千條語句,其執行時間也不過是一個較大的常數。
此類演算法的時間複雜度是O(1)。
測試2:
- (void)smallTest2:(int)n { int sum = 1; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { sum++; } } }
單純的雙層迴圈巢狀,複雜度 O(n^2)。
測試3:
- (void)smallTest3:(int)n { int y = 1; int x = 1; for (int i = 1 ; i < n; i++) { y++; // 語句 1 for (int j = 0; j < (2*n); j++) { x++; // 語句 2 } } }
語句1的頻度是n-1;
語句2的頻度是(n-1)*(2n+1) = 2n^2-n-1;
T(n) = 2n^2-n-1+(n-1) = 2n^2-2;
f(n) = n^2;
lim(T(n)/f(n)) = 2 + 2*(1/n^2) = 2;
T(n) = O(n^2).
測試4:
- (void)smallTest4:(int)n { int i = 1; // 語句 1 while (i <= n) { i = i*2; // 語句 2 } }
語句1的頻度是1,
設語句2的頻度是t, 則:2^t <= n; t <= log_2^n
考慮最壞情況,取最大值t=log_2^n,
T(n) = 1 + log_2^n
f(n) = log_2^n
lim(T(n)/f(n)) = 1/log_2^n + 1 = 1
T(n) = O(log n)
注: log_2^n表示 log 以 2 為底 n 的對數。
測試5:
- (void)smallTest5:(int)n { int x = 0; for(int i=0;i<n;i++) { for(int j=0;j<i;j++) { for(int k=0;k<j;k++) { x=x+2; } } } }
我沒有算出來這個數列的求總和到底是什麼公式。不過可以通過觀察,能知道這個演算法是三層迴圈巢狀,最內層是O(1)複雜度的執行語句,第二層和第三層不是以乘方遞減,所以最多是O(n^3)。如果哪位大神指導具體的求和公式是什麼還希望不吝賜教:grin:!