目录
问题描述:
假设有一序列,求得其子序列最大的和。比如,有一序列[-5, -3, 2, 4, -2, 5, -8, 7],那么其最大子序列和为9,其子序列为[2, 4, -2, 5,]
想法:
- 穷举。
- 利用递归“分而治之”。
第一版:
这一版像是为了增加时间复杂度而写出来的。
原理:
定义三层循环,最外层循环用来遍历整个列表,第二层循环用于遍历除了第一层循环遍历过的元素外的元素,最内层循环用于累加每次第一层循环遍历到的索引i到第二层循环遍历到的索引k之间的元素和。
代码:
int MaxSubsequenceSum_Version1(const int A[], const int N)
{
int ThisSum;
int MaxSum = 0;
for (int i = 0; i < N; i++)
{
for (int j = i; j < N; j++)
{
ThisSum = 0;
for (int k = i; k <= j; k++)
{
ThisSum += A[k];
}
//累加每次索引i到k之间的值,并储存在ThisSum中
if (ThisSum > MaxSum)
{
MaxSum = ThisSum;
}
//比较每次相加的和与当前认为最大子序列和
//如果当前和最大,那么便设置其为当前最大子序列和。
}
}
return MaxSum;
}
分析:
因为有三层嵌套循环,所以很容易判断出其时间复杂度为O(),在这种情况下,我们进行了很多不必要的计算(最内层的循环),所以我们可以去掉其中一个循环。
第二版:
原理:
在第一版的基础上去掉了一层循环。相同的,第一层循环用于遍历整个列表,但是第二层循环用于累加第一层遍历到的元素到列表最后,并在每次累加后判断当前子列表的和与已知最大子列表和,这样便省去了一个循环。
代码:
int MaxSubsequenceSum_Version2(const int A[], const int N)
{
int ThisSum;
int MaxSum = 0;
for (int i = 0; i < N; i++)
{
ThisSum = 0;
//每次相加索引i到k的子列表和
for (int j = i; j < N; j++)
{
ThisSum += A[j];
if (ThisSum > MaxSum)
{
MaxSum = ThisSum;
}
//比较每次相加的和与当前认为最大子序列和
//如果当前和最大,那么便设置其为当前最大子序列和。
}
}
return MaxSum;
}
分析:
有两层嵌套循环,因此很容易判断出其2时间复杂度为:O(),但是,算法中经常有“分而治之”的递归思想,有没有希望可以将两次嵌套循环变成递归加上并列的两次循环,这样便可以利用空间而降低时间复杂度。
第三版:
原理:
每次递归将当前序列分成前后两部分,其中有最大子序列和的情况只有下面三种情况:
- 最大子序列在左半部分。
- 最大子序列在右半部分。
- 最大子序列跨越了左右两部分。
我们总是对小于原问题的问题进行递归调用,而递归的调用是传入分成子序列的左右两边界,这样便可以避免重新定义数组所浪费的空间与拷贝所需要的时间,而判断退出递归的条件是当子序列只剩下一个元素的时候便直接返回那个元素。
代码:
int Max(const int A[], int Left, int Right)
{
int Center = (Left + Right)/2;
int MaxLeftSum = 0, MaxRightSum = 0;
int LeftBorderSum = 0, RightBorderSum = 0;
int MaxLeftBorderSum = 0, MaxRightBorderSum = 0;
/*判断基本状态,即只有一个元素的(左右两边界相等)的时候,也是退出递归的条件*/
if (Left == Right)
{
return A[Left] > 0 ? A[Left] : 0;
}
//开始递归,将当前序列分成两个子序列
MaxLeftSum = Max(A, Left, Center);
MaxRightSum = Max(A, Center + 1, Right);
/*计算两子序列到达边界处两个最大子序列和数的和,其为跨越边界的最大子序列和*/
for(int i = Center; i >= Left; i--)
{
LeftBorderSum += A[i];
if (LeftBorderSum > MaxLeftBorderSum)
{
MaxLeftBorderSum = LeftBorderSum;
}
}
for(int i = Center+1; i <= Right; i++)
{
RightBorderSum += A[i];
if (RightBorderSum > MaxRightBorderSum)
{
MaxRightBorderSum = RightBorderSum;
}
}
//判断三种情况下那一种情况最大,Max3()返回三个传入数值中最大的那个
return Max3(MaxRightSum, MaxLeftSum, MaxLeftBorderSum + MaxRightBorderSum);
}
int Max3(const int a, const int b, const int c)
{
int rt = a > b ? a : b;
rt = rt > c ? rt : c;
return rt;
}
int MaxSubsequenceSum_Version3(const int A[], const int N)
{
return Max(A, 0, N-1);
}
分析:
代码长并不代表算法不好,其中主要看的是嵌套循环的次数与调用递归的级数,可以很直观的判断出在判断其跨越边界的子序列和的时间复杂度都为O(n),而调用递归时所用的时间单元都是,所以即为O()所以整个算法的时间复杂度为O()。
第四版:
原理:
有没有可能只用一层循环便完成整个算法?答案当然是有的,只需要再加上一个条件判断便可以实现。首先创建两个变量用于存储当前已知最大子序列和,一个用于储存当前子序列和。只需要在每次循环时候判断当前子序列与当前已知最大子序列和哪个大就行了,而为了避免第一个数是负数,只需要判断当前子序列和是否小于0,小与的话便将其设为0。
代码:
int MaxSubsequenceSum_Version4(const int A[], const int N)
{
int ThisSum = 0;
int MaxSum = 0;
for (int i = 0; i < N; i++)
{
ThisSum += A[i];
//判断当前子序列和与已知最大子序列和的大小
if (ThisSum > MaxSum)
{
MaxSum = ThisSum;
}else if (ThisSum <0 )//判断当前子序列和是否小于0
{
ThisSum = 0;
}
}
return MaxSum;
}
分析:
可以很直观的看出整个算法只有一个循环,所以其时间复杂度为O(n),而其不像第三版递归那样用空间来换取时间,只占了常量存储空间,我们称这种只需要常量储存空间与花费线性时间的算法为“联机算法”,这种算法基本上是完美的算法。
参考书籍:【美】 马克 艾伦 维斯《数据结构与算法分析(原书第二版)》,侵删。