问题:给定N个整数的序列,求函数的最大值。
算法一:
例如序列为{1,2,3,4},所以子列分别为:{1},{1,2},{1,2,3},{1,2,3,4}; {2},{2,3},{2,3,4}; {3},{3,4}; {4}。我们要做的就是依次将这些子列的和求出并比较,得出最大子列和。
首先将“1”位置作为左端的所有子列进行比较,在依次进行递加的过程中比较。
1.当前子列为{1},当前子列和(ThisSum)为1,最大子列和(MaxSum)为0,将MaxSum赋值为ThisSum;
2.当前子列为{1,2},ThisSum = 3;MaxSum = 1; 3 > 1; 赋值:MaxSum = ThisMax = 3;
3.依次进行如上操作,直到子列为{1,2,3,4},ThisSum = 10;MaxSum = 6; 10>6; 赋值:MaxSum = ThisSum = 10;
(以上3步即为i的一次循环,得出了以“1”位置作为左端的所有子列的子列和最大为10)
4.然后继续以完全相同的方法进行之后的比较(分别以“2”,“3”,“4”位置作为左端
5.最后一次的当前子列为{4},ThisSum = 4;MaxSum = 10; 4 <10;不赋值;
6.最后返回MaxSum
程序如下:
int MaxSubseqSum1(int A[],int N)
{
int ThisSum;
int MaxSum = 0;
int i,j;
for(i = 0; i < N; i++){ //i是子列左端位置
ThisSum = 0; //求一个子列和之前,将上一个子列和归零
for(j = i; j < N; j++){ //j是子列右端
ThisSum += A[j];
ThisSum > MaxSum? MaxSum = ThisSum:MaxSum;
}
//一次i循环结束,就将“以同一个位置作为左端的所有子列”全部比较完毕
}
return MaxSum;
}
以上算法的时间复杂度为:.
看见,我们思考是否能够将时间复杂度降到,这便引出了下一个算法:分而治之。
算法二:
采取“分而治之”的方法,通过不断地将序列“对半分”,再分别将两边的部分“对半分”。
直到不能再分,再开始返回该部分的最大值,该部分的最大值可由“左半部分最大值,右半部分最大值,跨边界最大值”比较得出。
递归函数一次所执行的任务,简单地用流程描述为:
分两组找右最大找左最大找跨边界最大比较得出该组最大返回
找右/左最大:
1.如果整个数组只有两个数,则第一个数为右最大,第二个数为左最大。
2.如果整个数组长度大于二,执行递归,变 为右半部分(左半部分)找最大的问题。
找跨边界最大:
1.先从中间开始向左加,每次加一个数,找和为最大的左子列。
2.再从中间开始向右加,每次加一个数,找和为最大的右子列。
3.将左子列和与右子列和相加,即为跨边界最大的子列和。
例如序列为{4,-3,5,-2,-1,2,6,-2},先将其从中分为两部分,然后找右半部分的最大值。发现与之前整体找最大值归属于同一类问题,再将右边分为两部分,如上步骤分下去,直到数组元素只有两个。
然后从底层开始返回,找左最大,右最大以及跨边界最大
程序如下:
int MaxSubseqSum2(int A1[],int N1)
{
int MaxLeft = 0;//左最大
int MaxRight = 0;//右最大
int MaxCross = 0;//跨界最大
int ThisSum = 0;//求跨边界最大时的临时变量
int MaxCrossLeft = 0;//跨边界最大的左子列和
int MaxCrossRight = 0;// 跨边界最大的右子列和
int i,j;
int A[N1 + N1%2];// 当数字个数为奇数个数时,申请N+1个空间
int N;
if(N1%2 != 0){ //当N为奇数时
for(i = 0; i < N1; i++)
A[i] = A1[i];
A[N1] = 0; //将最后一个空间赋值为0,不影响最后结果
N = N1 + N1%2;
}else{ //当N为偶数时
for(i = 0; i < N1; i++)
A[i] = A1[i];
N = N1;
}
//将序列对半分:1.申请两组空间 2.进行赋值
int ALeft[N/2],ARight[N/2];
for(i = 0; i <= N/2;i++){
ALeft[i] = A[i];
ARight[i] = A[N/2 + i];
}
if(N == 2){ //当数字个数对半分,直到只剩两个数字时
A[0] > 0? MaxLeft = A[0]:MaxLeft; //左最大值赋值为第一个数
A[1] > 0? MaxRight = A[1]:MaxRight;//右最大值赋值为第二个数
}else{ //当数字个数仍大于2时
MaxLeft = MaxSubseqSum2(ALeft,N/2);//左最大值即求左半部分的最大值,与本函数研究问题一致,进行递归
MaxRight = MaxSubseqSum2(ARight,N/2);//右最大值,同上左最大值的处理
ThisSum = 0;
for(i = N/2-1; i >=0; i--){
ThisSum += A[i];
ThisSum > MaxCrossLeft? MaxCrossLeft = ThisSum:MaxCrossLeft;//求得跨边界最大的左子列和
}//从中间向左找最大
ThisSum = 0;
for(i = N/2; i < N; i++){
ThisSum += A[i];
ThisSum > MaxCrossRight? MaxCrossRight = ThisSum:MaxCrossRight;//求得跨边界最大的右子列和
}//从中间向右找最大
MaxCross = MaxCrossLeft + MaxCrossRight;//跨界最大=左子列和+右子列和
}
return MaxLeft > MaxRight?(MaxLeft > MaxCross?MaxLeft:MaxCross):(MaxRight > MaxCross?MaxRight:MaxCross);
//比较并得出该部分的最大子列和
}
以上算法的时间复杂度为:
求一个序列的最大子列和:1.求左半部分序列的最大子列和 2.求右半部分的最大子列和 3.求跨边界最大。所以可得出时间复杂度的递推公式为:
递推推导为:
其中
目前最好的算法,时间复杂度为:,那我们还有没有更好的算法,使时间复杂度降低到呢?这便引出了第三个算法:在线处理
算法三:
就问题先分析:从左往右依次累加。
1.如果一次累加得到的数为负数,再向后累加时,该数无法使得之后的累加值更大。所以干脆直接将该负数抛弃,取当前数为0.
2.如果一次累加得到的数为正数,无论这个数是加上前一个数得到的,还是减去一个数得到的,只要它仍为正数,它仍会使得之后的数更大,所以不抛弃。
3.采取以上的策略,每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。
程序如下:
int MaxSubseqSum3(int A[],int N)
{
int i;
int ThisSum = 0;
int MaxSum = 0;
for(i = 0; i < N; i++){
ThisSum += A[i]; //向右累加
if(ThisSum > MaxSum){
MaxSum = ThisSum; //发现更大和则更新当前结果
}else if(ThisSum < 0){ //如果当前子列和为负
ThisSum = 0; //则不可能使后面的部分和增大,抛弃之
}
}
return MaxSum;
}
上述算法3的时间复杂度为,可以说是该问题能够达到的最快的算法了。
测试主函数如下:
#include <stdio.h>
#define MaxN 7
int MaxSubseqSum1(int A[],int N);
int MaxSubseqSum2(int A1[],int N1);
int MaxSubseqSum3(int A[],int N);
int main()
{
int A[MaxN] = {4,-3,5,-2,-1,2,6};
printf("MaxSum1 = %d\n",MaxSubseqSum1(A,MaxN));
printf("MaxSum2 = %d\n",MaxSubseqSum2(A,MaxN));
printf("MaxSum3 = %d",MaxSubseqSum3(A,MaxN));
}
结果如下:
测试环境:win10 使用软件:DEV_C++
参考文献:陈越,《数据结构讲义》,浙江大学