時間複雜度學習(下)
2018年10月10日
這一節將以一個具體的演算法題給出4種不同解法,分析各自的時間複雜度並比較其各自的執行效能。
給出兩個求和公式,以下分析中會用到:
最大子序列和問題
,求 的最大值。(為方便起見,若所有整數均為負數,則最大子序列和為0)。
例如:輸入 ,其最大子序列和為 。
1,時間複雜度為 的解法
public static int maxSubSum1(int[] a) { int maxSum = 0; for (int i = 0; i < a.length; i++) { for (int j = i; j < a.length; j++) { int thisSum = 0; for (int k = i; k <= j; k++) { thisSum += a[k]; } if (thisSum > maxSum) { maxSum = thisSum; } } } return maxSum; } 複製程式碼
該種解法最簡單暴力,定義子序列的起始位置為 i
,結束位置為 j
,假設陣列a的長度為 N
,當 時, ,共 N
種情況,當 時, ,共 N-1
種情況,以此類推,當 時, ,僅此一種情況;將 i
與 j
之間的所有元素和記為 thisSum
,一旦 thisSum
的值比 maxSum
大,就更新 maxSum
的值為 thisSum
。
第一個迴圈大小為 N
,第二個迴圈大小為 N-i
,第三個迴圈大小為 j-i+1
,則總執行次數和為:
首先有:
接著:
那麼:
所以該種解法的時間複雜度為
2,時間複雜度為 的解法
public static int maxSubSum2(int[] a) { int maxSum = 0; for (int i = 0; i < a.length; i++) { int thisSum = 0; for (int j = i; j < a.length; j++) { thisSum += a[j]; if (thisSum > maxSum) { maxSum = thisSum; } } } return maxSum; } 複製程式碼
在第一種解法中,拿掉最裡面的那層迴圈,並稍做改動,就是現在的解法2。
其中第一層迴圈大小為 N
,第二層迴圈為 N-i
,則總執行次數為:
其中:
那麼:
所以第二種解法的時間複雜度為
3,時間複雜度為 的解法
如下圖所示,可以將陣列分為三部分,分別為前中後三部分。

最大子序列和就可能出現在這三個部分中,其中 ,前半部分是從 start
到 mid
這一部分的元素,即 ,所以該部分最大元素為 11
;後半部分是從 mid+1
到 end
這一部分的元素,即 ,所以該部分最大元素為 13
;而中間部分元素是以 mid
起始,分別向左和向右進行累加計算,分別求出其向左和向右部分的最大值,從 mid
向左得到其最大值: ,而向右是從 mid+1
開始算起得到其最大值: ,最後將左右兩部分和相加即為中間部分的最大值: ;比較前中後部分的最大值,發現中間部分的值 20
最大,所以該陣列最大啊子序列和為 20
。
那麼在程式中如何實現呢?這就要採用 分治策略
,將陣列 a
分為前後兩半子陣列 b,c
,再將前半陣列 b
分為前後兩半子陣列 d,e
,後半陣列 c
分為前後兩半子陣列 f,g
,……,直到陣列不能再分為止,此時子陣列中就只有一個元素,一個元素就好判斷了,該元素為正就直接把該元素值返回給上一級子陣列,為負就返回0,然後回到上一級子陣列,將之前返回的前後部分子陣列的最大值與中間部分最大值進行比較,得出其最大值,接著將最大值返回其上一級子陣列,直至回到原陣列,這時原陣列就得到了前後部分子陣列的最大值,接著求出中間部分子陣列的最大值並與前後部分進行比較即可得到整個陣列的最大子序列和。
public static int maxSubSum3(int[] a) { return a.length > 0 ? maxSumRec(a, 0, a.length - 1) : 0; } private static int maxSumRec(int[] a, int left, int right) { if (left == right) { if (a[left] > 0) { return a[left]; } else { return 0; } } int center = (left + right) / 2; int maxLeftSum = maxSumRec(a, left, center); int maxRightSum = maxSumRec(a, center + 1, right); int maxLeftBorderSum = 0; int leftBorderSum = 0; for (int i = center; i >= left; i--) { leftBorderSum += a[i]; if (leftBorderSum > maxLeftBorderSum) { maxLeftBorderSum = leftBorderSum; } } int maxRightBorderSum = 0; int rightBorderSum = 0; for (int i = center + 1; i <= right; i++) { rightBorderSum += a[i]; if (rightBorderSum > maxRightBorderSum) { maxRightBorderSum = rightBorderSum; } } return max3(maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum); } private static int max3(int a, int b, int c) { return a > b ? a > c ? a : c : b > c ? b : c; } 複製程式碼
其中 center
為陣列中間元素的下標, maxLeftSum
和 maxRightSum
分別為陣列前後部分的最大值, maxLeftBorderSum
為中間部分向左計算的最大值, maxRightBorderSum
為中間部分向右計算最大值; maxLeftBorderSum + maxRightBorderSum
即為中間部分的最大值。
計算中間部分,即計算 maxLeftBorderSum
和 maxRightBorderSum
總花費時間為 ,而計算前後兩半部分,即 maxLeftSum
和 maxRightSum
每個花費 個時間單元,則總共花費時間:
其中 ,則 , , , 。
那麼當 ,則 ,忽略低階項,所以該方法的時間複雜度為: 。
4,時間複雜度為 的解法
public static int maxSubSum4(int[] a) { int maxSum = 0; int thisSum = 0; for (int i = 0; i < a.length; i++) { thisSum += a[i]; if (thisSum > maxSum) { maxSum = thisSum; } else if (thisSum < 0) { thisSum = 0; } } return maxSum; } 複製程式碼
此種方法將時間複雜度優化到了 ,只需一輪迴圈即可找到最大子序列;其思路為:若當前子序列的和 thisSum
為負數,則將 thisSum
置為0,下一個陣列元素作為新的子序列的起始位置, thisSum
從該元素開始累加,直至找到最大子序列的和。
5,對比分析
使用下面程式碼測試上述4中解法所消耗的時間:
public static void getTimingInfo(int n, int alg) { int[] test = new int[n]; Random rand = new Random(); long startTime = System.currentTimeMillis(); long totalTime = 0; int i; for (i = 0; totalTime < 4000; i++) { for (int j = 0; j < test.length; j++) { test[j] = rand.nextInt(100) - 50; } switch (alg) { case 1: maxSubSum1(test); break; case 2: maxSubSum2(test); break; case 3: maxSubSum3(test); break; case 4: maxSubSum4(test); break; default: } totalTime = System.currentTimeMillis() - startTime; } System.out.print(String.format("\t%12.6f", (totalTime * 1000 / i) / (double) 1000000)); } public static void main(String[] args) { for (int n = 100; n <= 1000000; n *= 10) { System.out.print(String.format("N = %7d", n)); for (int alg = 1; alg <= 4; alg++) { if ((alg == 1 && n > 50000) || (alg == 2 && n > 500000)) { System.out.print("\tNA"); continue; } getTimingInfo(n, alg); } System.out.println(); } } 複製程式碼
執行結果如下圖,當預測時間過長,將其設為 NA
,從圖中可以看出,不同時間複雜度的程式雖然得出的結果是一樣的,但執行效能相差巨大,猶如波音與摩拜的差別。


總結:以後寫程式碼之前要多思考,避免一上來就暴力求解,造成巨大的效能開銷,應儘量將程式優化到線性階或線性對數階以內。