这是一道考的烂的不能再烂的题目,但是依然有很多公司乐于将这样的题目作为笔试或面试题,足见其经典。
问题是这样的:一个整数数组中的元素有正有负,在该数组中找出一个连续子数组,要求该子数组中各元素的和最大,这个子数组便被称作最大子数组。比如数组{2,4,-7,5,2,-1,2,-4,3}的最大子数组为{5,2,-1,2},最大子数组的和为5+2-1+2=8。
下面按照时间复杂度逐步优化的顺序依次给出这三种算法。
暴力求解法
该方法的思想非常简单,先找出从第1个元素开始的最大子数组,而后再从第2个元素开始找出从第2个元素开始的最大子数组,依次类推,比较得出最大的子数组。
1 /* 2 常规方法,时间复杂度O(n*n) 3 先从第一个元素开始向后累加, 4 每次累加后与之前的和比较,保留最大值, 5 再从第二个元素开始向后累加,以此类推。 6 */ 7 int MaxSubSum1(int *arr,int len) 8 { 9 int i,j; 10 int MaxSum = 0; 11 //每次开始累加的起始位置的循环 12 for(i=0;i<len;i++) 13 { 14 int CurSum = 0; 15 //向后累加的循环 16 for(j=i;j<len;j++) 17 { 18 CurSum += arr[j]; 19 if(CurSum > MaxSum) 20 MaxSum = CurSum; 21 } 22 } 23 return MaxSum; 24 }
很明显地可以看出,该方法的时间复杂度为O(n*n)。
分治求解法
所谓分治法,是指将一个问题分解为两个子问题,然后分而解决之。具体步骤如下:
-
先将数组分为两个等长的子数组a, b;
-
分别求出两个数组a,b的连续子数组之和;
-
还有一种情况比较容易忽略:有可能最大和的子数组跨越两个数组;
float maxsum3(l, u)
if (l > u) /* zero elements */
return 0
if (l == u) /* one element */
return max(0, x[l])
m = (l + u) / 2
/* find max crossing to left */
lmax = sum = 0
for (i = m; i >= l; i--)
sum += x[i]
lmax = max(lmax, sum)
/* find max crossing to right */
rmax = sum = 0
for i = (m, u]
sum += x[i]
rmax = max(rmax, sum)
return max(lmax+rmax,
maxsum3(l, m),
maxsum3(m+1, u));
容易证明,时间复杂度为 O(nlogn)。
考虑将数组从中间分为两个子数组,则最大子数组必然出现在以下三种情况之一:
1、完全位于左边的数组中。
2、完全位于右边的数组中。
3、跨越中点,包含左右数组中靠近中点的部分。
递归将左右子数组再分别分成两个数组,直到子数组中只含有一个元素,退出每层递归前,返回上面三种情况中的最大值。
1 /* 2 求三个数中的最大值 3 */ 4 int Max3(int a,int b,int c) 5 { 6 int Max = a; 7 if(b > Max) 8 Max = b; 9 if(c > Max) 10 Max = c; 11 return Max; 12 } 13 14 /* 15 次优算法,采用分治策略 16 */ 17 int MaxSubSum2(int *arr,int left,int right) 18 { 19 int MaxLeftSum,MaxRightSum; //左右边的最大和 20 int MaxLeftBorderSum,MaxRightBorderSum; //含中间边界的左右部分最大和 21 int LeftBorderSum,RightBorderSum; //含中间边界的左右部分当前和 22 int i,center; 23 24 //递归到最后的基本情况 25 if(left == right) 26 if(arr[left]>0) 27 return arr[left]; 28 else 29 return 0; 30 31 //求含中间边界的左右部分的最大值 32 center = (left + right)/2; 33 MaxLeftBorderSum = 0; 34 LeftBorderSum = 0; 35 for(i=center;i>=left;i--) 36 { 37 LeftBorderSum += arr[i]; 38 if(LeftBorderSum > MaxLeftBorderSum) 39 MaxLeftBorderSum = LeftBorderSum; 40 } 41 MaxRightBorderSum = 0; 42 RightBorderSum = 0; 43 for(i=center+1;i<=right;i++) 44 { 45 RightBorderSum += arr[i]; 46 if(RightBorderSum > MaxRightBorderSum) 47 MaxRightBorderSum = RightBorderSum; 48 } 49 50 //递归求左右部分最大值 51 MaxLeftSum = MaxSubSum2(arr,left,center); 52 MaxRightSum = MaxSubSum2(arr,center+1,right); 53 54 //返回三者中的最大值 55 return Max3(MaxLeftSum,MaxRightSum,MaxLeftBorderSum+MaxRightBorderSum); 56 } 57 58 /* 59 将分支策略实现的算法封装起来 60 */ 61 int MaxSubSum2_1(int *arr,int len) 62 { 63 return MaxSubSum2(arr,0,len-1); 64 }
设该算法的时间复杂度为T(n),则:
T(n)= 2T(n/2)+ O(n),且T(1)= 1。
逐步递推得到时间复杂度T(n)= O(nlogn)。
线性时间算法
该算法在每次元素累加和小于0时,从下一个元素重新开始累加。
1 /* 2 最优方法,时间复杂度O(n) 3 和最大的子序列的第一个元素肯定是正数 4 因为元素有正有负,因此子序列的最大和一定大于0 5 */ 6 int MaxSubSum3(int *arr,int len) 7 { 8 int i; 9 int MaxSum = 0; 10 int CurSum = 0; 11 for(i=0;i<len;i++) 12 { 13 CurSum += arr[i]; 14 if(CurSum > MaxSum) 15 MaxSum = CurSum; 16 //如果累加和出现小于0的情况, 17 //则和最大的子序列肯定不可能包含前面的元素, 18 //这时将累加和置0,从下个元素重新开始累加 19 if(CurSum < 0) 20 CurSum = 0; 21 } 22 return MaxSum; 23 }
显然,该算法的时间复杂度O(n)。该算法理解起来应该不难,但是要想出来可就不容易了。另外,该算法的一个附带的有点是:它只对数据进行一次扫描,一旦元素被读入并被处理,它就不再需要被记忆。因此,如果数组在磁盘或磁带上,他就可以被顺序读入,在主存中不必存储数组的任何部分。不仅如此,在任意时刻,该算法都能对它已经读入的数据给出最大子数组(另外两种算法不具有这种特性)。具有这种特性的算法叫做联机算法。
动态规划:(f(i)为存储0-i子数组的最大和)
1 #include <iostream> 2 using namespace std; 3 4 5 int FindSerialMaxSum(int* pData,int nLength,int* f) 6 { 7 f[0]=pData[0]; 8 int MaxSum=0; 9 for(int i=1;i<nLength;i++) 10 { 11 if(f[i-1]<=0) 12 { 13 f[i]=pData[i]; 14 } 15 else 16 { 17 f[i]=f[i-1]+pData[i]; 18 } 19 20 if(f[i]>MaxSum) 21 MaxSum=f[i]; 22 } 23 for(int k=0;k<nLength;k++) 24 { 25 cout<<f[k]<<" "; 26 } 27 cout<<endl; 28 return MaxSum; 29 } 30 31 int main(int argc ,char* argv[]) 32 { 33 int pdata[]={1,-2,3,10,-4,7,2,-5}; 34 int nLength=8; 35 int *f=new int[nLength]; 36 int SerivalSum=FindSerialMaxSum(pdata,nLength,f); 37 if(SerivalSum!=0) 38 cout<<"The Serial Max Sum = "<<SerivalSum<<endl; 39 else 40 cout<<"Input error!"<<endl; 41 delete[] f; 42 system("pause"); 43 return 0; 44 }