给定(可能有负的)整数A1,A2..An,求最大子序列之和。(为了方便起见,如果所有整数均为负数,则最大子序列和为0)
例如:对于输入-2,11,-4,13,-5,-2,答案为20(从A2 到A4)
算法分析:
算法1:该算法肯定会正确运行,运行时间为O(n*3)(最简单,但是也是运行最慢的算法,后面会讲解原因)
class Solution {
public int maxSubSuml(int[] a) {
int maxSum=0;
for (int i = 0; i < a.length; i++) {
//从头开始扫描
for (int j = i; j < a.length; j++) {
int thisSum=0;
//每次都以i为开始下标,以j为结束下标
for (int k = i; k < j; k++) {
thisSum+=a[k];
}
if(thisSum>maxSum){
maxSum=thisSum;
}
}
}
return maxSum;
}
}
class Solution {
public int maxSubSuml(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:采用折半查找思想
分析:
在我们的例子中,最大子序列和可能在三处出现。或者整个出现在输入数据的左半部,或者整个出现在右半部,或者跨越输入数据的中部从而位于左右两部分之中。
前两种情况可以递归求解。第三种情况的最大和可以通过求出前半部分(包含前半部分最后一个元素)的最大和以及后半部分(包含半部分第一个元素)的最大和而得到。
此时将这两个和想家。作为一个例子,考入如下输入:
前半部 | 后半部分
4 -35-2| -1 26-2
前半部分最大和为6(从元素A1到A3),而后半部分的最大子序列之和为8(从元素A5到A7)。
前半部分包含其最后一个元素的最大和是4(从元素A1到A4),而后半部分包含其第一个元素的最大和是7(从元素A5到A7)。因此,横跨这两部分且通过中间的最大和为4+7=11(从元素A1到A7)。
我们看到,在形成本例中的最大和子序列的三种方式中,最好的方式是包含两部分的元素。于是,答案为11。算法如下:
class Solution {
public int maxSubRec(int[] a,int left,int right) {
//该算法通过中间向两边查找最大的子序列
//只有一个元素
if(left==right){
if(a[left]>0)
return a[left];
else
//所有整数均为负数,结果返回0
return 0;
}
int mid=(left+right)/2;
//左边最大值
int maxLeftSum=maxSubRec(a,left,mid);
//右边最大值
int maxRightSum=maxSubRec(a, mid+1, right);
//从左边开始
//从中间边界开始寻找
int maxLeftBorderSum=0,leftBorderSum=0;
for(int i=mid;i>=left;i--){
leftBorderSum+=a[i];
if(leftBorderSum>maxLeftBorderSum){
maxLeftBorderSum=leftBorderSum;
}
}
//从右边开始
int maxRightBorderSum=0, rightBorderSum=0;
for(int i=mid+1;i<=right;i++){
rightBorderSum+=a[i];
if(rightBorderSum>maxRightBorderSum){
maxRightBorderSum=rightBorderSum;
}
}
//返回结果
//比较三者最大值 (左边 右边 中间)
//maxLeftBorderSum为前半部分包含其到最后一个元素的最大值
//maxRightBorderSum为后半部分包含其第一个元素的最大值
//maxLeftBorderSum+maxRightBorderSum为中间部分最大值
return Math.max(Math.max(maxLeftSum, maxRightSum), maxLeftBorderSum+maxRightBorderSum);
}
}
算法4:联机算法(*****推荐*****)
分析:一个结论是,如果a[i]是负的,那么它不可能代表最优序列的起点,因为任何包含a[i]的作为起点的子序列都可以通过用a[i+1]作起点而得到改进。
类似的,任何负的子序列不可能是最优子序列的前缀(原理相同)。如果在内循环中检测到从a[i]到a[j]的子序列是负的,那么可以推进i。关键的结论是,
我们不仅能够把i推进到i+1,而且实际还可以把它一直推进到j+1。为了看清楚这一点,令p为i+1和j之间的任一下标。开始于下标p的任意子序列都不大于
在下标i开始并包含从a[i]到a[p-1]的子序列的对应的子序列,因为后面这个子序列不是负的(j是是的从下标i开始其值成为负值的序列的第一个下标)。因此
,把i推进到j+1是没有风险的:我们一个最优解也不会错过。
class Solution {
public int maxSubRec(int[] a,int left,int right) {
int maxSum=0,thisSum=0;
for(int j=0;j<a.length;j++){
thisSum+=a[j];
if(thisSum>maxSum){
maxSum=thisSum;
}else if(thisSum<0){
//关键这步,因为如果和小于0,表示前面为负数,可以重新置为0
thisSum=0;
}
}
return maxSum;
}
}
说明:这个算法是许多聪明算法的典型:运行时间是明显的,但正确性则不容易看出来。对于这些算法,正式的正确性证明(比上面的分析更正式)几乎总是需要的;然而
,即使到那时,许多人仍能还是不信服。此外,许多这类算法需要更有技巧的编程,这导致更长的开发过程。不过当这些算法正常工作时,它们运行的很快,而我们将它们和一个低效(但容易实现)的蛮力算法通过小规模的输入进行比较可以测试到大部分的程序原理。
该算法的一个附带的有点事,它只对数据进行一次扫描,一旦a[i]被读入并被处理,它就不再需要被记忆。因此,如果数组在磁盘上或通过互联网传送,那么它就可以被按顺序读入,在主存中不必存储数组的任何部分。不仅如此,在任意时刻,算法都能对它已经读入的数据给出子序列问题的正确答案。具有这种特性的算法叫做联机算法,仅需要常量空间并以先行时间运行的联机算法几乎是完美的算法。