问题描述:
输入一个整型数组,数组里有正数也有负数。数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。求所有子数组的和的最大值。当所有的输入都是负数时,总和最大的子数组是空数组,总和为0。例如输入数组包含下面10个元素:
那么该程序的输出为x[2...6]的总和,即187.
题外话:
有人说编程珠玑这本书是经典,我也认同,但有人也说这本书是让人费脑筋提前掉头发的书,我更认同,有句话我想说,书中很多地方的描述非常绕,绕到你很难理解,即便理解了也很难给人讲明白。该问题就是很好的一个例子。下面首先用书中的思路解决该问题,再用另外一种思路解决,其实两种解决办法是完全一样的,但思路真的千差万别,孰优孰劣,读者自鉴。
Pearl解题思路:
假设我们已解决了x[0...i-1]的问题,即我们找到了x[0...i-1]中最大子数组和,那么只需要扩展到x[i]就可以解决这个问题了。根据类似于分治算法的原理:前i个元素中,最大总和子数组要么在前i-1个元素中(我们将其存储在maxsofar中),要么其结束位置为i(我们将其存储在maxendinghere中)。
Pearl代码实现
int FindMaxSumOfVectorPearl(int *pData, int nLength)
{
int maxsofar = 0; /* max sum of the vector*/
int maxendinghere = 0; /* candidate max sum of the vector */
if(NULL == pData || nLength <= 0)
{
return 0;
}
for(int i = 0; i < nLength; i++)
{
/* invariant: maxsofar and maxendinghere are accurate for x[0...i-1]*/
maxendinghere = max(maxendinghere + pData[i], 0);
maxsofar = max(maxsofar, maxendinghere);
}
return maxsofar;
}
理解为个程序的关键就在于变量maxendinghere。在循环中的第一个赋值语句之前,maxendinghere是结束位置为i-1的最大子数组的和;赋值语句将其修改为结束位置为i的最大子数组的和。若加上x[i]之后结果依然为正值,则该赋值语句使maxendinghere增大x[i];若加上x[i]之后结果为负值,该赋值语句就将maxendinghere重新设为0(因为结束位置为i的最大子数组现在为空数组)。
国人解题思路:
当我们加上一个正数时,和会增加;当我们加上一个负数时,和会减少。如果当前得到的和是个负数,那么这个和在接下来的累加中应该抛弃并重新清零,不然的话这个负数将会减少接下来的和。
国人代码实现:
int FindMaxSumOfVectorMine(int *pData, int nLength)
{
int maxsofar = 0; /* max sum of the vector*/
int maxendinghere = 0; /* candidate max sum of the vector */
if(NULL == pData || nLength <= 0)
{
return 0;
}
for(int i = 0; i < nLength; i++)
{
maxendinghere += pData[i];
if(maxendinghere <= 0)
{
maxendinghere = 0;
continue;
}
/* if a greater sum is found, update the greatest sum */
if(maxendinghere > maxsofar)
{
maxsofar = maxendinghere;
}
}
return maxsofar;
}
我认为下面的思路更好懂一些,上面的代码更简短一些。
总结
这个题目中涉及了一个很重要的算法设计技术:保存状态,避免重复计算。比如本题,最笨的办法或许是枚举出所有子数组并求出它们的和,这种思路增加了太多的重复计算,时间复杂度为O(n3)。引申一下,在求Fibonacci数列的问题中,递归算法一直做为教材中经典案例,但是这个算法也有重复计算的问题,该算法的改良版正是着力解决了重复计算而使算法更加高效。求n的阶乘也同样适用。