一个一维int数组,这个数组有很多子数组,那么子数组之和的最大值是什么呢?
思考
1、题目说的子数组是连续的;
2、题目只需要求和,并不需要返回子数组的具体位置;
3、数组的元素是整数,所以数组可能包含有正整数、零、负整数;
举几个例子:
数组:[1, -2, 3, 5, -3, 2]应返回:8
数组:[0, -2, 3, 5, -1, 2]应返回:9
数组:[-9, -2, -3, -5, -3]应返回:-2,这也是最大子数组的和。
【解法一】:
直接的方法,记Sum[i,…,j]为数组A中第i个元素的第j个元素的和(其中0<=i<=j<n),遍历所有可能的Sum[i,…j],那么时间复杂度为O(NNN):
int MaxSum(int* A, int n)
{
int maxmum = -INF;
int sum;
for (int i = 0; i < n; i++)
{
for (int j = i; j < n; j++)
{
for (int k = i; k<=j; k++)
{
sum += A[k];
}
if (sum > maxmum)
maxmum = sum;
}
}
return maxmum;
}
如果注意到Sum[i,…j] = Sum[i,…j-1] + A[j],则可以将算法中的最后一个for循环省略,避免重复计算,从而使算法得以改进,改进后的算法如下,这时复杂度为O(N*N):
int MaxSum(int* A, int n)
{
int maxmum = -INF;
int sum;
for (int i = 0; i < n; i++)
{
for (int j = i; j < n; j++)
{
for (int k = i; k<=j; k++)
{
sum += A[k];
}
if (sum > maxmum)
maxmum = sum;
}
}
return maxmum;
}
【解法二】:如果将所给数组(A[0],…,A[n-1])分为长度相等的两段数组(A[0],…,A[n/2-1])和(A[n/2],…A[n-1]),分别求出这两段数组各自的最大子段和,则原数组(A[0],…A[n-1])的最大子段和为以下三种情况的最大值:
1、(A[0],…A[n-1])的最大子段和与(A[0],…,A[n/2-1])的最大子段和相同;
2、(A[0],…A[n-1])的最大子段和与(A[n/2],…A[n-1])的最大子段和相同;
3、(A[0],…A[n-1])的最大子段跨过其中两个元素A[n/2-1]和A[n/2];
第1和2两种情况事实上是问题规模减半的相同子问题,可以通过递归求得。
至于第3中情况,我们只要找到以A[n/2-1]结尾的和最大的一段数组之和s1 = (A[i],…A[n/2-1])(0 <= i < n/2-1)和以A[n/2]开始和最大的一段数组之和s2 = (A[n/2],…A[j])(n/2 <= j < n)。那么第3种情况的最大值为s1 + s2 = A[i]+…+A[n/2-1]+A[n/2]+…+A[j],只须要对原数组进行一次遍历即可。
其实这是一种分治算法,每个问题都可分解成为两个问题规模减半的子问题,在加上一次遍历算法。该分治算法的时间复杂度满足典型的分治算法递归式,总的时间复杂度为T(N) = O(N*lgN)。
【解法三】:解法二中的分治算法已经将时间复杂度从O(NN)降到了O(NlgN),应该说是一个不错的改进,但是否还可以进一步讲时间复杂度降低呢?答案是肯定的,从分治算法中得到提示:可以考虑数组的第一个元素A[0],以及最大的一段数组(A[i],…A[j])跟A[0]之间的关系,有一下几种情况:
1、当0 = i = j时,元素A[0]本身构成和最大的一段;
2、当0 =i < j时,和最大的一段以A[0]开始;
3、当0 < i时,元素A[0]跟和最大的一段没有关系;
从上面三种情况可以看出,可以将一个大问题(N个元素数组)转化为一个较小的问题(n-1个元素的数组)。假设已经知道(A[1],…,A[n-1])中和最大的一段数组之和为All[1],并且已经知道(A[1],…,A[n-1])中包含A[1]的和最大的一段数组为Start[1]。那么,根据上述分析的三种情况,不难看出(A[0],…,A[n-1])中问题的解All[0]是三种情况的最大值max{A[0],A[0]+Start[1],All[1]}。通过这样的分析,可以看出这个问题符合无后效性,可以使用动态规划的方法来解决。
int max (int x, int y) // 返回x,y中较大值
{
return (x > y) ? x : y;
}
int MaxSum (int* A, int n)
{
Start[n-1] = A[n-1];
All[n-1] = A[n-1];
for (i = n-2; i >= 0; i--)
{
Start[i] = max(A[i], A[i] + Start[i+1]); // 从数组末尾往前遍历直到数组首
All[i] = max(Start[i], All[i+1]);
}
return All[0]; // 遍历完数组,All[0]中存放着结果
}
新方法的时间复杂度已经降到O(N)了。
但一个新的问题出现了:我们额外申请了两个数组All[]、Start[],能否在空间方面也节省一点呢?
观察这两个递推式:
1 Start[i] = max{A[i], Start[i+1] + A[i]};
2 All[i] = max{Start[i], All[i+1]};
第一个递推式:Start[i] = max{A[i], Start[i+1] + A[i]}。如果Start[i+1] < 0,则Start[i] = A[i]。而且,在这两个递推式中,其实都只须要用两个变量就可以了。Start[k+1]只有在计算Start[k]时使用,而All[k+1]也只有在计算All[k]时使用。所以程序可以进一步改进一下,只需O(1)的空间就足够了。
int max (int x, int y)
{
return (x > y) ? x : y;
}
int MaxSum (int* A, int n)
{ // 要做参数检查
nStart = A[n-1];
nAll = A[n-1];
for (i = n-2; i >= 0; i--)
{
nStart = max(A[i], nStart + A[i]);
nAll = max(nStart, nAll);
}
return nAll;
}
改进的算法不仅节省了空间,而且只有寥寥几行,却达到了很高的效率。我们还可以换一个写法:
int MaxSum (int* A, int n)
{ // 要做参数检查
nStart = A[n-1];
nAll = A[n-1];
for (i = n-2; i >= 0; i--)
{
if (nStart < 0)
nStart = 0; // 检查全部是负数,如何?
nStart += A[i];
if (nStart > nAll)
nAll = nStart;
}
return nAll;
}
这篇博客探讨了如何找到一维整数数组中子数组之和的最大值。提出了三种解法:直接遍历法(O(N^2))、分治法(O(N log N))和动态规划法(O(N))。动态规划法是通过维护两个变量在遍历过程中计算最大和,同时解决了空间复杂度问题。
1116

被折叠的 条评论
为什么被折叠?



