问题描述
给定一个序列(整数或浮点数),求出其中连续的子序列和最大的那一个。
例:序列{-10 1 2 3 4 -5 -23 3 7 -21},其最大的连续子序列为{1 2 3 4}或{3 7},最大和为10.
算法1:穷举法
最直观的算法,穷举式地尝试所有可能。下标变量i表示子序列的开始位置,j表示结束位置,每次选定一个子序列Ai--Aj,再使用k遍历该子序列求子序列的和。代码如下:
public static int maxSubSum(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;
}
算法分析:最外层的for循环大小为N,第二个for循环大小为N-i,假设为最坏的情况即为N,这可能会让最终的界有些大,但不影响结果。第三个循环的大小为j-i+1,我们也假设为N,因此总数为O(1·N·N·N)=O(N3)。
算法2:改进后的穷举法
从算法1可看出,j其实是从i递增的,因而在内部再遍历一次多余,所以可撤除最内层的for循环来避免三次的运行时间,显然改进后的算法复杂度为O(N2),代码如下:
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;
}
算法3: 分治+递归
考虑使用分治法解决该问题。最大子序列出现的位置可能有3种情况,或者全部出现在左半部,或者全部出现在右半部,或者跨越中部从而位于左右两半部分之中。前两种情况可以递归求解,第三种情况可以通过求出前半部分(包括前半部分最后一个元素)的最大和以及后半部分(包括后半部分第一个元素)的最大和,将两者相加即可得到。
public static int maxSubSum3(int[] a){
return maxSumRec(a, 0, a.length-1);
}
private static int maxSumRec(int[] a,int left,int right){
if(left==right) //base class
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,leftBorderSum=0;
//累加求左边界和
for(int i=center;i>=left;i--){
leftBorderSum+=a[i];
if(leftBorderSum>maxLeftBorderSum){
maxLeftBorderSum=leftBorderSum;
}
}
int maxRightBorderSum=0,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){
int max;
max=a>b?a:b;
max=max>c?max:c;
return max;
}
算法分析:在遍历求maxLeftBorderSum和maxRightBorderSum时,两个for循环合起来花费的时间为O(N),而在递归求maxLeftSum和maxRightSum时,实质是在分别求解N/2的子序列问题(假设N是偶数),所以每个递归花费T(N/2)个时间单元,因此有递归方程:T(1)=1,T(N)=2T(N/2)+O(N)。因此有,T(2)=4=2*2,T(4)=12=4*3,T(8)=32=8*4,以及T(16)=80=16*5,可以推出,若N=2k,有T(N)=N*(k+1)=NlogN+N=O(NlogN)。(当N不是2的幂时,分析会复杂些,但是大O的结果是不变的)
算法4:动态规划--联机算法
可以发现,负数不可能为最优子序列(和最大的子序列)的起点,因此可以推出,任何负的子序列(和为负,形如[4,-5])不可能是最优子序列的前缀。如果在内循环中检测到从a[i]到a[j]的子序列是负的,那么可以把它一直推进到j+1,如6,1,2,-10,1,可以推进到-10后的1,舍弃6到-10,在程序中通过将thisSum置零实现推进。该算法复杂度为O(N),很明显可以看出。
public int maxSubSum4(int[] a){
int maxSum=0,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;
}
总结:算法1、2想法简单粗暴,复杂度较高,算法3需要理解递归分治的思想,算法4需要一定的技巧,是很多聪明算法的典型,而且算法4只需对数据进行一次扫描,一旦a[i]被读入且被处理后,就不再需要被记忆,在任意时刻,算法4能对已经读入的数据给出子序列问题的正确答案(其他算法不具有该特性),具有这种特性的算法叫做联机算法(on-line algorithm)。仅需常量空间并以线性时间运行的联机算法几乎是完美的算法。