问题描述:
给定一个n个元素的整型数组,求出最大的连续子数组的始末位置与最大连续子数组的和。
问题分析:
这个问题在网上有很多资料可以参考,可惜大部分资料是不完善或者有错误的,在《算法导论》里面这个问题是用来介绍分治算法的,但是这道题作为常考的面试题,从里面可以学到的东西还是很多的,所以在这里学习一下,有什么问题请多指正。
首先我们应该考虑的是边界条件和特殊情况:
1.如果有多个最大子数组,那么返回起始位置最小的;
2.如果全是负数,返回最大的负数。
下面我们再逐步分析思路:
我们先定义要保存信息的结构体和系统最大值:
#define INF (2<<30-1)
typedef struct maxDate{
int max;
int start;
int end;
}maxDate;
穷举法
一般在遇到求数组的子数组时,我们第一个想到的就是穷举,穷举所有的子数组比较。
//该算法的思路是穷举法,穷举子数组的起始位置i,终止位置j,
//然后再遍历求和i到j与最大值比较,时间复杂度O(n^3)
maxDate maxSum(int* a, int n)
{
maxDate mD;
int maximum = -INF;
int sum=0;
int p=0,q=0;
for(int i = 0; i < n; i++)
{
for(int j = i; j < n; j++)
{
sum=0;//注意每一次穷举时都要先把sum清0
for(int k = i; k <= j; k++)
{
sum += a[k];
}
if(sum > maximum){
maximum = sum;
p = i;
q = j;
}
}
}
mD.max = maximum;
mD.start = p;
mD.end = q;
return mD;
}
然后我们看看穷举是否可以优化,答案是肯定的。实际上连续子数组有n(n+1)/2个,穷举的最优化解法也只能做到O(n^2)
//优化,遍历相加实际上可以用到开始位置i和结束位置j,时间复杂度是O(n^2)
maxDate maxSum(int* a, int n)
{
maxDate mD;
int maximum = -INF;
int sum=0;
int p=0,q=0;
for(int i = 0; i < n; i++)
{
sum = 0;
for(int j = i; j < n; j++)
{
sum += a[j];
if(sum > maximum){
maximum = sum;
p = i;
q = j;
}
}
}
mD.max = maximum;
mD.start = p;
mD.end = q;
return mD;
}
分治法
《算法导论》里面介绍的分治法可以将复杂度降到O(nlog2n),其思路就是:连续最大子数组只可能有三种情况:
完全位于左半边,完全位于右半边或者横跨中点左右两边都有。那么我们利用递归可以划分左右部分和中部求它们各自的最大子数组,然后再比较它们的最大和,和最大的那个就是整个数组的最大子数组。
//分治法,时间复杂度O(nlog2n)
maxDate crossSumMax(int a[], int low, int high)
{
//求包含中间数的最大子数组和,一个以k结尾的最大数组最大和要么只是包含它自己,
// 要么是它加上上一个的最大和,即maxSum = max{k, k+maxSum[k-1]}
maxDate mD;
int mid = (low+high)/2;
int i;
int max1 = -INF;
int max2 = -INF;
int sum1 = 0;
int sum2 = 0;
for (i=mid; i>=low; i--)
{
sum1+=a[i];
if (sum1 > max1){
max1 = sum1;
mD.start = i;
}
}
for (i=mid+1; i<=high; i++)
{
sum2+=a[i];
if (sum2 > max2){
max2 = sum2;
mD.end = i;
}
}
mD.max = max1+max2;
return mD;
}
maxDate maxSum(int a[], int low, int high)
{
maxDate mD;
if (low == high){
mD.start = mD.end = low;
return mD;
}
int mid = (high+low)/2;//划分
maxDate leftMax = maxSum(a, low, mid);//求左边的最大数组
maxDate rightMax = maxSum(a, mid+1, high);//求右边的最大数组
maxDate crossMax = crossSumMax(a, low, high);//求中间的最大数组
//比较三个最大值的大小并返回最大的,即合并过程
if (leftMax.max>=rightMax.max && leftMax.max>=crossMax.max)
{
return leftMax;
}
else if (rightMax.max>leftMax.max && rightMax.max>crossMax.max)
return rightMax;
else
return crossMax;
}
分治算法的时间复杂度分析:注意这里我们分析递归的时候,左右的划分复杂度是2T(n/2),中间的复杂度是O(n)
T(n) = 2T(n/2)+O(n)+O(1)
由递归树分析或者由《算法导轮》上的公式可以得出复杂度是O(nlog2n);
我们发现虽然写出了复杂度更低的算法,但是明显太复杂了,而且递归的开销使得这个算法在n很小的时候并不实用。作者写下来仅是为了让我们理解分治而已。
动态规划
动态规划可以做到最优解O(n)。思路就是上面我们求包含中间数的最大子数组的思路,只不过现在我们只要每次都包含最后一个数来分析就行了。从第一个数开始,如果前面的最大和是负数,那么加上它,整个最大和肯定会减小,所以我们舍去,把它置为最大和,如果是正数,那肯定增加,我们就加上它。代码描述的更加清楚:
maxDate maxSum2(int a[], int n)
{
maxDate mD;
int sum = 0;
int max = -INF;
int p, q;//p,q是数组的起末位置
mD.start = mD.end = p = q = 0;
int i;
for (i=0; i<n; i++)
{
if (sum < 0){
p = q = i;
sum = a[i];
}
else{
q = i;
sum += a[i];
}
if (sum > max)
{
max = sum;
mD.start = p;
mD.end = q;
}
}
mD.max = max;
return mD;
}
注意这里保存位置信息的时候做了处理。
总结:遇到数组的问题我们一般会想到的就是这三种方法,其中动态规划要注意适用条件。