算法
算法(algorithm)是為求解一個問題需要遵循的、被清楚地指定的簡單指令的集合。
數學基礎
四個定義
如果存在正常數 c 和 n0 使得當 N ≥ n0時,T(N) ≤ cf(N),則記為T(N) = O(f(N))。
(描述了T(N)的相對增長率小於等於f(N)的相對增長率。)
2. 大Ω表示法:
如果存在正常數 c 和 n0 使得當 N ≥ n0時,T(N) ≥ cf(N),則記為T(N) = Ω(f(N))。
(描述了T(N)的相對增長率大於等於f(N)的相對增長率。)
3. 大Θ表示法:
如果 T(N) = O(f(N)) 且 T(N) = Ω(f(N)),則 T(N) = Θ(f(N))。
(描述了T(N)的相對增長率等於f(N)的相對增長率。)
4. 小o表示法:
如果 T(N) = O(f(N)) 且 T(N) ≠ Θ(f(N)),則 T(N) = o (f(N))。
(描述了T(N)的相對增長率小於f(N)的相對增長率。)
三個結論
1. 如果T1(N) = O(f(N)) 且 T2(N) = O(g(N)),那么
(a). 加法法則:T1(N) + T2(N) = max(O(f(N)), O(g(N)));【大O的和等於大O的最大值】
(b). 乘法法則:T1(N) * T2(N) = O(f(N) * g(N)).【大O的積等於積的大O】
2. 如果T(N) 是一個k次多項式,則T(N) = Θ(Nk).
3. 對任意常數k,logkN = O(N)。它告訴我們對數增長得非常緩慢。
時間復雜度
一個算法在輸入規模為N時運行的耗時稱為時間復雜度,常用大O表示。一般來說,它描述了最壞情況下的時間復雜度(平均情況下的時間復雜度需要更加復雜的數學分析)。
為了簡化分析,約定:不存在特定的時間單位。因此,常拋棄一些常數系數和低階項,從而便於計算大O運行時間。
看個例子:
int sum(int N) { int i, partialSum; partialSum = 0; //1 個時間單元 for (i = 1; i < N; i++) //初始化耗時 1 個時間單元,測試比較耗時 N + 1 個時間單元,自增運算耗時 N 個時間單元 partialSum += i * i * i; //4 個時間單元(2 次乘,1 次加,1 次賦值),循環 N 次耗時 4N 個時間單元 return partialSum; //1 個時間單元 }
聲明不耗時間,忽略函數調用和返回值的開銷,總共耗時1 + 1 + N + 1 + N + 4N+ 1 = 6N + 4。按照之前的約定,忽略低階項4和常系數6,我們說該函數是O(N),時間復雜度是線性級。
這僅僅是一個小函數,如果有一個較大的程序,那么計算時間復雜度需要的工作量就太瑣碎繁雜了。考慮大O的結果,它只關注得到的最高階項。常數級運行時間相對於有關輸入規模N的語句的耗時是很小的(無關緊要),所以忽略掉常數級O(1)的語句第5行、第7行、第8行,跟輸入規模N有關的耗時主要是for循環,循環大小為N,所以該函數的運行時間就是O(N)線性級的。
計算時間復雜度的一般法則
法則1——for循環
一次for循環的運行時間至多是該for循環內語句(包括測試)的運行時間乘以迭代的次數。
法則2——嵌套的for循環
從里向外分析這些循環。在一組嵌套循環內部的一條語句總的運行時間為該語句的運行時間乘以該組所有for循環的大小的乘積。
法則3——順序語句
將各個語句的運行時間求和即可(這意味着,其中的最大值就是所得的運行時間)
法則4——if/else 語句
一個if/else語句的運行時間從不超過判斷再加上分支語句中運行時間長者的總的運行時間。顯然在某些情況下這么估計有些過高,但絕不會估計過低。
最大子序列和問題
問題描述:給定整數A1,A2,,... ,AN(可能有負數),求∑jk=i Ak的最大值(為方便起見,如果所有整數均為負數,則最大子序列和為0)。
下面給出四種算法。
1. 窮舉法
枚舉所有的子序列之和,返回最大值。時間復雜度O(n3)。
int maxSequenceSum1(const int A[], int N) { int i, j, k, maxSum, thisSum; maxSum = 0; for (i = 0; i < N; i++) { for (j = i; j < N; j++) { thisSum = 0; for (k = i; k <= j; k++) thisSum += A[k]; if (thisSum > maxSum) maxSum = thisSum; } } return maxSum; }
2. 撤銷一個for循環,降低立方級的運行時間
考慮到∑jk=i Ak = ∑j-1k=i Ak + Aj。修改如下。算法復雜度O(N2)。
int maxSequenceSum2(const int A[], int N) { int i, j, maxSum, thisSum; maxSum = 0; for (i = 0; i < N; i++) { thisSum = 0; for (j = i; j < N; j++) { thisSum += A[j]; if (thisSum > maxSum) maxSum = thisSum; } } return maxSum; }
3. 分治算法
把一個問題分成兩個大致相等的子問題,然后遞歸地對它們求解,這是“分”部分。“治”階段將兩個子問題的解合到一起並可能再做少量的附加工作,最后得到整個問題的解。
思路:最大子序列和只可能出現在三處:左半部分、右半部分、跨越並穿過中間而占據左右兩半部分。前兩種情況可以遞歸求解,第三部分的最大和可以通過求出前半部分的最大和(包括前半部分最后一個元素)以及后半部分的最大和(包括后半部分第一個元素)而得到,然后將這兩個和加在一起。時間復雜度O(logN)。
考慮下列輸入:
前半部分 | 后半部分 |
4 -3 5 -2 | -1 2 6 -2 |
其中前半部分的最大子序列和為6(從元素A1到A3)而后半部分的最大子序列和為8(從元素A6到A7)。
前半部分包含其最后一個元素的最大和是4(從元素A1到A4),而后半部分包含其第一個元素的最大和是7(從元素A5到A7)。因此,跨越這兩部分且通過中間的最大和為4+7 = 11(從元素A1到A7)。
int maxSubSum(const int A[], int left, int right) { int maxLeftSum, maxRightSum; int maxLeftBorderSum, maxRightBorderSum; int leftBorderSum, rightBorderSum; int center, i; if (left == right) /*Base case*/ { if (A[left] > 0) return A[left]; else return 0; } center = (left + right) / 2; maxLeftSum = maxSubSum(A, left, center); maxRightSum = maxSubSum(A, center + 1, right); maxLeftBorderSum = 0; leftBorderSum = 0; for (i = center; i >= left; i--) { leftBorderSum += A[i]; if (leftBorderSum > maxLeftBorderSum) maxLeftBorderSum = leftBorderSum; } maxRightBorderSum = 0; rightBorderSum = 0; for (i = center + 1; i <= right; i++) { rightBorderSum += A[i]; if (rightBorderSum > maxRightBorderSum) maxRightBorderSum = rightBorderSum; } return max(maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum); } int maxSequenceSum3(const int A[], int N) { return maxSubSum(A, 0, N - 1); }
4.聯機算法
每個數據只訪問一次。僅需要常量空間並以線性時間運行的聯機算法幾乎是完美的算法。所以聯機算法的時間復雜度是O(N)
int maxSequenceSum4(const int A[], int N) { int i, maxSum, thisSum; maxSum = 0; thisSum = 0; for (i = 0; i < N; i++) { thisSum += A[i]; if (thisSum> maxSum) maxSum = thisSum; else if (thisSum < 0) thisSum = 0; } return maxSum; }
時間復雜度中的對數規律
某些分治算法將以O(NlogN)運行。除分治算法外,可將對數最常出現的規律概括為以下一般法則:
如果一個算法用常數時間O(1)將問題的大小削減為其一部分(通常是1/2),那么該算法就是O(logN)的。另一方面,如果使用常數時間只是把問題減少一個常數(如將問題減少1)那么這種算法那就是O(N)的。
具有對數特點的三個例子
三個例子的時間復雜度均為O(logN)。
1.對分查找
給定一個整數X和A0,A1,... ,AN-1,后者已經預先排序並在內存中,求使得Ai = X的下標i,如果X不在數據中,則返回i = -1。
int binarySearch(const int A[], int N, int X) { int low, high, mid; low = 0;high = N - 1; while (low <= high) { mid = (low + high) / 2; if (A[mid] < X) low = mid + 1; else if (A[mid] > X) high = mid - 1; else return mid; } return -1; //not found }
2. 歐幾里德算法
計算最大公因數。兩個整數的最大公因數(Gcd)是同時整除兩者的最大整數。
算法通過連續計算余數直到為 0 時停止,最后的非零余數就是最大公因數。
unsigned int gcd(unsigned int M, unsigned int N) { int rem; while (N > 0) { rem = M % N; M = N; N = rem; } return M; }
3.冪運算
計算XN。
如果N是偶數,則XN = X(N/2) * X(N/2);如果N是奇數,則XN = X(N-1/2) * X(N-1/2) * X。
long pow(long X, unsigned int N) { if (N == 0) return 1; if (N == 1) return X; if (isEven(N)) return pow(X * X, N / 2); else return pow(X * X, N / 2) * X; }
(完)