最大子序列和問題
最大子列和問題是非常經典的問題,基本上講算法的書都會將這個例子,用此例題來講解算法時間復雜度的重要性,對比不同算法的時間復雜度。最大子列和問題如下:給定整數序列A1,A2,A3,A4,...,An(可能存在負數),求A(i)+A(i+1)+........+A(j)的最大值(無法輸入公式),請看下圖:![]()
注:為了方便起見,如果所有的整數均為負數,則最大的子序列和為0
算法的運行時間
這個問題之所以有如此的吸引力,主要是因為存在求解它的很多算法,而且這些算法的性能又差異很大。我們將討論求解該問題的四種算法。這四種算法的運行時間如下表所示:(算法1是O(N^3),圖中寫錯了)

- 表中的幾個重要的情況值得注意。對於小量的輸入,算法可以在眨眼之間的完成,因而如果只是小量輸入的情況下,那么花費大量的時間去設計優秀的算法恐怕是不值得的。另一方面,隨着業務,用戶的增加,小量輸入的情況可能會發生變化,哪些低效率的程序可能必須要進行重寫。
- 其次,表中所給的時間不包括讀入數據的所需要的時間,對於算法4,僅僅從磁盤讀入數據所用的時間很可能在數量級上比求解問題所需的時間還要大。數據的讀入一般是一個瓶頸,一旦數據讀入,問題就會迅速解決。但是對於低效的算法,它必然要耗費大量的計算資源。因此,只要可能,使得算法足夠有效而不至於成為問題的瓶頸是非常重要的。
我們還可以通過函數曲線來對這四種算法的時間復雜度函數進行分析,通過曲線我們清楚的可以看出O(nlgn)算法時間復雜度是介於O(n^2)的O(n)之間的,當然這也不難證明。在實際的情況中,當我們采用O(n^2)算法的時候,應該在仔細想想,能否將算法的時間復雜度優化成O(nlgn),這對算法的性能提升也是非常巨大的,不妨要問,為什么不優化為O(n)呢?事實上,O(n)時間復雜度意味着只需要進行一次掃描,就能找到問題的解,在大部分的問題中,這是非常的困難的。
O(n^3)算法
1 #include<iostream> 2 #include<stdio.h> 3 using namespace std; 4 5 int MaxSubsequenceSum(int a[],int n); 6 7 int main(){ 8 //int a[6] = {-2, 11, -4, 13, -5, -2}; 9 int a[8] = {4, -3, 5, -2, -1, 2, 6, -2}; 10 printf("%d\n",MaxSubsequenceSum(a,8)); 11 } 12 13 int MaxSubsequenceSum(int a[],int n){ 14 int ThisSum, MaxSum; 15 MaxSum = 0; 16 for(int i = 0; i < n; i++){ 17 for(int j = i; j < n; j++){ 18 ThisSum = 0; 19 for(int k = i; k <= j; k++){ 20 ThisSum += a[k]; 21 } 22 if(ThisSum > MaxSum){ 23 MaxSum = ThisSum; 24 } 25 } 26 } 27 return MaxSum; 28 }
這是一種O(n^3)的解法,說實話,我是寫不來這樣高時間復雜度的算法,這個算法重復做了很多的無用的計算,強行將算法復雜化,經過簡單的分析,直接可以求 ThisSum += a[k] 語句的次數,就能夠得出它的時間復雜度:

O(n^2)算法
對上述的算法直接優化,我們發現最里面的循環是完全多余的,很過分的消耗了大量的時間,很容易就能得到下面的算法
1 int MaxSubsequenceSum(int a[],int n){ 2 int ThisSum, MaxSum; 3 MaxSum = 0; 4 for(int i = 0; i < n; i++){ 5 ThisSum = 0; 6 for(int j = i; j < n; j++){ 7 ThisSum += a[j]; 8 if(ThisSum > MaxSum){ 9 MaxSum = ThisSum; 10 } 11 } 12 } 13 return MaxSum; 14 }
相信大部分人首想想到的應該是這個算法把,這個算法性能只能說還行。但是,我們想到了O(n^2)的時候,應該多思考一下,能否將其轉化為O(nlogn)呢?如果能的話,這將會極大的提高算法的性能。
O(nlogn)算法
如果沒有O(n)算法的話,那么遞歸的威力就能體現出來了。這個算法采用的是分治策略,分治思想是把所求問題划分成兩個大致相等的問題,然后遞歸的對它進行求解,這是分的思想,治的階段是將兩個子問題的解合並到一起,最后得到整個問題的解。
在這個問題中,最大的子序列和可能出現在三處,要么是序列的左半部分,要么是序列的右半部分,要么是跨越輸入數據的中間左右部分都有,前面的兩種情況可以用遞歸進行求解,第三種情況的最大子序列和可以通過求出前半部分的最大和以及后半部分的最大和而得到,我們可以通過下面的例子進行分析:

- 前半部分最大子序列和為6,
- 后半部分的最大子序列和為8。
- 前半部分包含最后一個元素的最大和是4,而后半部分包含第一個元素的的最大和是7,因此跨越兩部分的最大和是11,這是最大的子列和。
這個算法的源碼有點復雜,仔細讀幾遍。
1 int MaxSubSum(int A[], int Left, int Right){ 2 int MaxLeftSum, MaxRightSum; 3 int MaxLeftBorderSum, MaxRightBorderSum; 4 int LeftBorderSum, RightBorderSum; 5 int Center; 6 if(Left == Right){ 7 if(A[Left] > 0){ 8 return A[Left]; 9 }else{ 10 return 0; 11 } 12 } 13 14 Center = (Left + Right) / 2; 15 MaxLeftSum = MaxSubSum(A, Left, Center); //遞歸求解左半部分的最大和 16 MaxRightSum = MaxSubSum(A, Center + 1, Right); //遞歸求解右半部分的最大和 17 18 MaxLeftBorderSum = 0; 19 LeftBorderSum = 0; 20 for(int i = Center; i >= Left; i--){ 21 LeftBorderSum += A[i]; 22 if(LeftBorderSum > MaxLeftBorderSum){ 23 MaxLeftBorderSum = LeftBorderSum; 24 } 25 } 26 27 MaxRightBorderSum = 0; 28 RightBorderSum = 0; 29 for(int i = Center+1; i <= Right; i++){ 30 RightBorderSum += A[i]; 31 if(RightBorderSum > MaxRightBorderSum){ 32 MaxRightBorderSum = RightBorderSum; 33 } 34 } 35 return Max3(MaxLeftBorderSum+MaxRightBorderSum,MaxLeftSum,MaxRightSum); 36 } 37 38 int Max3(int a, int b, int c){ 39 if(a>b){ 40 return a > c ? a : c; 41 }else{ 42 return b > c ? b : c; 43 } 44 } 45 46 int MaxSubsequenceSum(int a[],int n){ 47 return MaxSubSum(a, 0, n-1); 48 }
時間復雜度分析
有興趣的同學可以參考網易公開課:麻省理工學院公開課:算法導論,第三集分治法,講的非常詳細,還有推導過程。
O(n)算法
1 int MaxSubsequenceSum(int a[],int n){ 2 int ThisSum = 0, MaxSum = 0; 3 for(int j = 0; j < n; j++){ 4 ThisSum += a[j]; 5 if(ThisSum > MaxSum){ 6 MaxSum = ThisSum; 7 }else if (ThisSum < 0){ 8 ThisSum = 0; //ThisSum < 0,說明跨越a[j]不能使序列和變大 9 } 10 } 11 return MaxSum; 12 }
這個算法的效率非常的高,又被稱為在線處理算法,算法只需要掃描一遍序列,就能找到最大的子序列和,它的技巧就是一旦A[i]被讀入並被處理,它就不再需要被記憶。不僅如此,在任意時刻,算法都能夠對它已經讀入的數據給出正確的答案。具有這種特性的算法叫做聯機算法。僅需要常量的空間並以線性時間運算的聯機算法集合是完美的算法。
