最简单暴力的就是两层遍历,求出每个子序列的和,然后取其最大值。
文中给出了另外一种算法,被提炼称呼为“贪心算法”,但题解并未给出另外一个更有用的数据:
子序和有了,起止索引在哪
?
与其上来就七上八下用一套武功招式将这题破解,我更倾向于从基本思想着手,自然地创立出招式,从而将题目破解。我们接下来将用简单的逻辑,用实际情况对逻辑进行升级和修正,最后得出真正可用的方法。
假如数组为-1 2 -1 2 -3,我们肉眼一眼就看出最大子序和3,序列为2 -1 2。OK,现在从逻辑角度来计算最大子序和。
从正常思维来看,首先第一个-1就要被排除,为什么?我作为一个整数,去和一个-1即负数相加,肯定会使得我变得更小,我还加它干什么?-1跳过!
接下来是2,加2会使得整数更大,保留。
接下来是-1,问题来了:第一个-1被跳过了,这个-1怎么处理?显然这个-1不能直接跳过,为什么?因为序列是连续的,-1如果被废弃,那前面的2不也被丢掉了吗?!如果不废弃,应该怎么处理它?因为前面的2被保留了,所以要想使用-1,必须连带2一起用,2 + -1 = 1 > 0,所以2和-1组成的组合值得被保留。
wait!事情发展到这里,好像和开始的思想发生点了变化?我们开始认为负数应当舍弃,正数应当保留,而现在要把2 -1作为一个整体,这意味着什么?意味着我们处理的目标从单个数值变成了一组数值!我们这里称为小数组。
小数组的和>0,则我们认为将它纳入子序列是有价值的。
小数组的和<0,则它会拖累我们,使得子序和变小,丢弃!
将思路进一步精炼:每次处理一个数据的时候,我们都判断之前小数组的和是否为负数,如果它是负数,将它丢掉!从这个数据开始重新扫描;如果它是正数,则相加,然后继续向后扫描。
于是我们有以下伪代码
int maxSubArray(vector<int>& nums)
{
int sum = nums[0];
for (int i = 0; i < nums.size(); ++i)
{
if (sum < 0)
{ //之前序列小于0,丢弃!重新扫描
sum = nums[i];
}
else
{//之前序列大于等于0,可以保留
sum += nums[i];
}
}
}
一个重要的问题:上面代码中并没有保存最大值,OK,我们添加一个max作为最大值。理想情况下,如果sum一直连续,则max将被更新为最新的sum,但如果sum被打断重新开始了,max是不是也要重新开始呢?我们持怀疑态度,如数组2 -1 -3 1,前三个数据和为-2,于是从第4个1重新开始,但1好像比第一个数2还小?看来这种情况max不一定要更新嘛。Good!对于这种情况的处理,我们可以归结为将max设置为sum和max的最大值,如果新的sum比max还小,我们就保持max不变!又因为我们已经将第一个数作为默认的和了,所以循环从1开始,于是有如下代码
int maxSubArray(vector<int>& nums)
{
int sum = nums[0];
int max = sum;
for (int i = 1; i < nums.size(); ++i)
{
if (sum < 0)
{ //之前序列小于0,丢弃!重新扫描
sum = nums[i];
}
else
{//之前序列大于等于0,可以保留
sum += nums[i];
}
//无论是丢弃情况,还是保留情况,都需要和max作对比
//丢弃情况:对于2 -1 -3 10来说,到10时,前面和为-2全部丢弃,需要对比10和2,取10为最大值
//保留情况,对于2 -1 -3来说,到-1时,和为1,需要和2对比,取2为最大值
if (sum > max)
max = sum;
}
return max;
}
至此,我们已经循着开始不完善的思想走到了现在,可以说已经解决了这个问题。我们查看一下LeetCode的解决代码
public int maxSubArray(int[] num) {
int length = num.length;
int cur = num[0];
int max = cur;
for (int i = 1; i < length; i++) {
cur = Math.max(cur, 0) + num[i];
//记录最大值
max = Math.max(max, cur);
}
return max;
}
形式有差异,但做的事情是一模一样的。
PS:本文没有处理最大子序列的起止索引,有兴趣的同学可以交流一下^_^