原文:http://www.cnblogs.com/gaochundong/p/complexity_of_algorithms.html
爲何要進行算法分析?html
預測算法所需的資源
計算時間(CPU 消耗)
內存空間(RAM 消耗)
通訊時間(帶寬消耗)
預測算法的運行時間
在給定輸入規模時,所執行的基本操做數量。
或者稱爲算法複雜度(Algorithm Complexity)
如何衡量算法複雜度?c++
內存(Memory)
時間(Time)
指令的數量(Number of Steps)
特定操做的數量
磁盤訪問數量
網絡包數量
漸進複雜度(Asymptotic Complexity)
算法的運行時間與什麼相關?算法
取決於輸入的數據。(例如:若是數據已是排好序的,時間消耗可能會減小。)
取決於輸入數據的規模。(例如:6 和 6 * 109)
取決於運行時間的上限。(由於運行時間的上限是對使用者的承諾。)
算法分析的種類:數組
最壞狀況(Worst Case):任意輸入規模的最大運行時間。(Usually)
平均狀況(Average Case):任意輸入規模的期待運行時間。(Sometimes)
最佳狀況(Best Case):一般最佳狀況不會出現。(Bogus)
例如,在一個長度爲 n 的列表中順序搜索指定的值,則網絡
最壞狀況:n 次比較
平均狀況:n/2 次比較
最佳狀況:1 次比較
而實際中,咱們通常僅考量算法在最壞狀況下的運行狀況,也就是對於規模爲 n 的任何輸入,算法的最長運行時間。這樣作的理由是:數據結構
一個算法的最壞狀況運行時間是在任何輸入下運行時間的一個上界(Upper Bound)。
對於某些算法,最壞狀況出現的較爲頻繁。
大致上看,平均狀況一般與最壞狀況同樣差。
算法分析要保持大局觀(Big Idea),其基本思路:dom
忽略掉那些依賴於機器的常量。
關注運行時間的增加趨勢。
好比:T(n) = 73n3 + 29n3 + 8888 的趨勢就至關於 T(n) = Θ(n3)。函數
漸近記號(Asymptotic Notation)一般有 O、 Θ 和 Ω 記號法。Θ 記號漸進地給出了一個函數的上界和下界,當只有漸近上界時使用 O 記號,當只有漸近下界時使用 Ω 記號。儘管技術上 Θ 記號較爲準確,但一般仍然使用 O 記號表示。優化
使用 O 記號法(Big O Notation)表示最壞運行狀況的上界。例如,ui
線性複雜度 O(n) 表示每一個元素都要被處理一次。
平方複雜度 O(n2) 表示每一個元素都要被處理 n 次。
Notation Intuition Informal Definition
f is bounded above by g asymptotically
Two definitions :
Number theory:
f is not dominated by g asymptotically
Complexity theory:
f is bounded below by g asymptotically
f is bounded both above and below by g asymptotically
例如:
T(n) = O(n3) 等同於 T(n) ∈ O(n3)
T(n) = Θ(n3) 等同於 T(n) ∈ Θ(n3).
至關於:
T(n) 的漸近增加不快於 n3。
T(n) 的漸近增加與 n3 同樣快。
複雜度 標記符號 描述
常量(Constant)
O(1)
操做的數量爲常數,與輸入的數據的規模無關。
n = 1,000,000 -> 1-2 operations
對數(Logarithmic)
O(log2 n)
操做的數量與輸入數據的規模 n 的比例是 log2 (n)。
n = 1,000,000 -> 30 operations
線性(Linear) O(n)
操做的數量與輸入數據的規模 n 成正比。
n = 10,000 -> 5000 operations
平方(Quadratic) O(n2)
操做的數量與輸入數據的規模 n 的比例爲二次平方。
n = 500 -> 250,000 operations
立方(Cubic) O(n3)
操做的數量與輸入數據的規模 n 的比例爲三次方。
n = 200 -> 8,000,000 operations
指數(Exponential)
O(2n)
O(kn)
O(n!)
指數級的操做,快速的增加。
n = 20 -> 1048576 operations
注1:快速的數學回憶,logab = y 其實就是 ay = b。因此,log24 = 2,由於 22 = 4。一樣 log28 = 3,由於 23 = 8。咱們說,log2n 的增加速度要慢於 n,由於當 n = 8 時,log2n = 3。
注2:一般將以 10 爲底的對數叫作經常使用對數。爲了簡便,N 的經常使用對數 log10 N 簡寫作 lg N,例如 log10 5 記作 lg 5。
注3:一般將以無理數 e 爲底的對數叫作天然對數。爲了方便,N 的天然對數 loge N 簡寫作 ln N,例如 loge 3 記作 ln 3。
注4:在算法導論中,採用記號 lg n = log2 n ,也就是以 2 爲底的對數。改變一個對數的底只是把對數的值改變了一個常數倍,因此當不在乎這些常數因子時,咱們將常常採用 "lg n"記號,就像使用 O 記號同樣。計算機工做者經常認爲對數的底取 2 最天然,由於不少算法和數據結構都涉及到對問題進行二分。
而一般時間複雜度與運行時間有一些常見的比例關係:
複雜度 10 20 50 100 1000 10000 100000
O(1)
<1s
<1s
<1s
<1s
<1s
<1s
<1s
O(log2(n))
<1s
<1s
<1s
<1s
<1s
<1s
<1s
O(n)
<1s
<1s
<1s
<1s
<1s
<1s
<1s
O(n*log2(n))
<1s
<1s
<1s
<1s
<1s
<1s
<1s
O(n2)
<1s
<1s
<1s
<1s
<1s
2s
3-4 min
O(n3)
<1s
<1s
<1s
<1s
20s
5 hours
231 days
O(2n)
<1s
<1s
260 days
hangs
hangs
hangs
hangs
O(n!)
<1s
hangs
hangs
hangs
hangs
hangs
hangs
O(nn)
3-4 min
hangs
hangs
hangs
hangs
hangs
hangs
計算代碼塊的漸進運行時間的方法有以下步驟:
肯定決定算法運行時間的組成步驟。
找到執行該步驟的代碼,標記爲 1。
查看標記爲 1 的代碼的下一行代碼。若是下一行代碼是一個循環,則將標記 1 修改成 1 倍於循環的次數 1 n。若是包含多個嵌套的循環,則將繼續計算倍數,例如 1 n * m。
找到標記到的最大的值,就是運行時間的最大值,即算法複雜度描述的上界。
示例代碼(1):
複製代碼
1 decimal Factorial(int n)
2 {
3 if (n == 0)
4 return 1;
5 else
6 return n * Factorial(n - 1);
7 }
複製代碼
階乘(factorial),給定規模 n,算法基本步驟執行的數量爲 n,因此算法複雜度爲 O(n)。
示例代碼(2):
複製代碼
1 int FindMaxElement(int[] array)
2 {
3 int max = array[0];
4 for (int i = 0; i < array.Length; i++)
5 {
6 if (array[i] > max)
7 {
8 max = array[i];
9 }
10 }
11 return max;
12 }
複製代碼
這裏,n 爲數組 array 的大小,則最壞狀況下須要比較 n 次以獲得最大值,因此算法複雜度爲 O(n)。
示例代碼(3):
複製代碼
1 long FindInversions(int[] array)
2 {
3 long inversions = 0;
4 for (int i = 0; i < array.Length; i++)
5 for (int j = i + 1; j < array.Length; j++)
6 if (array[i] > array[j])
7 inversions++;
8 return inversions;
9 }
複製代碼
這裏,n 爲數組 array 的大小,則基本步驟的執行數量約爲 n*(n-1)/2,因此算法複雜度爲 O(n2)。
示例代碼(4):
複製代碼
1 long SumMN(int n, int m)
2 {
3 long sum = 0;
4 for (int x = 0; x < n; x++)
5 for (int y = 0; y < m; y++)
6 sum += x * y;
7 return sum;
8 }
複製代碼
給定規模 n 和 m,則基本步驟的執行數量爲 n*m,因此算法複雜度爲 O(n2)。
示例代碼(5):
複製代碼
1 decimal Sum3(int n)
2 {
3 decimal sum = 0;
4 for (int a = 0; a < n; a++)
5 for (int b = 0; b < n; b++)
6 for (int c = 0; c < n; c++)
7 sum += a b c;
8 return sum;
9 }
複製代碼
這裏,給定規模 n,則基本步驟的執行數量約爲 nnn ,因此算法複雜度爲 O(n3)。
示例代碼(6):
複製代碼
1 decimal Calculation(int n)
2 {
3 decimal result = 0;
4 for (int i = 0; i < (1 << n); i++)
5 result += i;
6 return result;
7 }
複製代碼
這裏,給定規模 n,則基本步驟的執行數量爲 2n,因此算法複雜度爲 O(2n)。
示例代碼(7):
斐波那契數列:
Fib(0) = 0
Fib(1) = 1
Fib(n) = Fib(n-1) + Fib(n-2)
F() = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ...
複製代碼
1 int Fibonacci(int n)
2 {
3 if (n <= 1)
4 return n;
5 else
6 return Fibonacci(n - 1) + Fibonacci(n - 2);
7 }
複製代碼
這裏,給定規模 n,計算 Fib(n) 所需的時間爲計算 Fib(n-1) 的時間和計算 Fib(n-2) 的時間的和。
T(n<=1) = O(1)
T(n) = T(n-1) + T(n-2) + O(1)
fib(5) / \ fib(4) fib(3) / \ / \ fib(3) fib(2) fib(2) fib(1) / \ / \ / \
經過使用遞歸樹的結構描述可知算法複雜度爲 O(2n)。
示例代碼(8):
複製代碼
1 int Fibonacci(int n)
2 {
3 if (n <= 1)
4 return n;
5 else
6 {
7 int[] f = new int[n + 1];
8 f[0] = 0;
9 f[1] = 1;
10
11 for (int i = 2; i <= n; i++)
12 {
13 f[i] = f[i - 1] + f[i - 2];
14 }
15
16 return f[n];
17 }
18 }
複製代碼
一樣是斐波那契數列,咱們使用數組 f 來存儲計算結果,這樣算法複雜度優化爲 O(n)。
示例代碼(9):
複製代碼
1 int Fibonacci(int n)
2 {
3 if (n <= 1)
4 return n;
5 else
6 {
7 int iter1 = 0;
8 int iter2 = 1;
9 int f = 0;
10
11 for (int i = 2; i <= n; i++)
12 {
13 f = iter1 + iter2;
14 iter1 = iter2;
15 iter2 = f;
16 }
17
18 return f;
19 }
20 }
複製代碼
一樣是斐波那契數列,因爲實際只有前兩個計算結果有用,咱們可使用中間變量來存儲,這樣就不用建立數組以節省空間。一樣算法複雜度優化爲 O(n)。
示例代碼(10):
經過使用矩陣乘方的算法來優化斐波那契數列算法。
複製代碼
1 static int Fibonacci(int n)
2 {
3 if (n <= 1)
4 return n;
5
6 int[,] f = { { 1, 1 }, { 1, 0 } };
7 Power(f, n - 1);
8
9 return f[0, 0];
10 }
11
12 static void Power(int[,] f, int n)
13 {
14 if (n <= 1)
15 return;
16
17 int[,] m = { { 1, 1 }, { 1, 0 } };
18
19 Power(f, n / 2);
20 Multiply(f, f);
21
22 if (n % 2 != 0)
23 Multiply(f, m);
24 }
25
26 static void Multiply(int[,] f, int[,] m)
27 {
28 int x = f[0, 0] m[0, 0] + f[0, 1] m[1, 0];
29 int y = f[0, 0] m[0, 1] + f[0, 1] m[1, 1];
30 int z = f[1, 0] m[0, 0] + f[1, 1] m[1, 0];
31 int w = f[1, 0] m[0, 1] + f[1, 1] m[1, 1];
32
33 f[0, 0] = x;
34 f[0, 1] = y;
35 f[1, 0] = z;
36 f[1, 1] = w;
37 }
複製代碼
優化以後算法複雜度爲O(log2n)。
示例代碼(11):
在 C# 中更簡潔的代碼以下。
複製代碼
1 static double Fibonacci(int n)
2 {
3 double sqrt5 = Math.Sqrt(5);
4 double phi = (1 + sqrt5) / 2.0;
5 double fn = (Math.Pow(phi, n) - Math.Pow(1 - phi, n)) / sqrt5;
6 return fn;
7 }
複製代碼
示例代碼(12):
插入排序的基本操做就是將一個數據插入到已經排好序的有序數據中,從而獲得一個新的有序數據。算法適用於少許數據的排序,時間複雜度爲 O(n2)。
複製代碼 1 private static void InsertionSortInPlace(int[] unsorted) 2 { 3 for (int i = 1; i < unsorted.Length; i++) 4 { 5 if (unsorted[i - 1] > unsorted[i]) 6 { 7 int key = unsorted[i]; 8 int j = i; 9 while (j > 0 && unsorted[j - 1] > key)10 {11 unsorted[j] = unsorted[j - 1];12 j--;13 }14 unsorted[j] = key;15 }16 }17 }複製代碼