最大子数组的和问题–线性算法

计算给定数组的最大子数组的和有很多种算法,最常见的是使用分治的策略,然而此问题用分治却增加了时间复杂度和代码复杂度。有更简单的算法,本文就将介绍一个线性时间的迭代算法。这应该是最高效的解决方法了。 首先代码如下:

int maxSubArray( int* array, int length)
{
    int boundry = array[0];
    int maxArray = array[0];
    for( int i=1; i<length; ++i )
    {
        if( boundry+array[i] >= array[i] ) 
            boundry += array[i];
        else
            boundry = array[i];
        if( maxArray < boundry )
            maxArray = boundry; 
    }
    return maxArray;
}

#include<iostream>
using namespace std;
int main()
{
    int a[] = {1,-2,3,10,-4,7,2,-48};
    int num = sizeof(a)/sizeof(a[0]);
    int result = maxSubArray(a, num);
    cout<<"result:"<<result<<endl;//输出结果为18

    int b[] = {3,-1,5,-1,9,-20,21,-20,20,21};
    num = sizeof(b)/sizeof(b[0]);
    result = maxSubArray(b, num);
    cout<<"result:"<<result<<endl;//输出结果为42
    return 0;
}

整个函数只有12行,如此的简单却高效

思路详解

解题思路来源于算法导论习题4.1-5

使用如下思想为最大子数组问题设计一个非递归的、线性时间的算法。从数组的左边界开始,从左至右处理,记录到目前为止已经处理过的最大子数组。若已知A[1..j]的最大子数组,基于如下性质将解扩展为A[1..j+1]的最大子数组:A[1..j+1]的最大子数组要么是A[1..j]的最大子数组,要么是某个子数组A[i..j+1] (1≤i≤j+1)。在已知A[1..j]的最大子数组的情况下,可以在线性时间内找出形如A[i..j+1]的最大子数组。

注意:本文只讨论最大子数组的和的问题,所以后文提到最大子数组时,都是指最大子数组的和。

为了与习题里面叙述保持一致,本节讨论思路时下标都从1开始,A[1]表示第一个元素。

已知 A[1..1] 的最大子数组是第一个元素,要么 A[1..2] 的最大子数组要么是 A[1..1] 的最大子数组,要么是 A[i..2] 的最大子数组。换个说法就是 A[1..2] 的最大子数组要么包含第二个元素,要么不包含第二个元素;所以①需要从包含第二个元素和不包含第二个元素的两种情况里面选一个最大的值出来。

不包含第二个元素的值是可以确定的,就是 A[1..1] 的最大子数组,是已知的;为了方便起见,称之为前最大子数组。而包含第二个元素的最大子数组需要另外去算;为了方便起见,我们称之为边界最大子数组。我们真正需要的最大子数组就是 前最大子数组 和边界最大子数组中值较大的一个。

那么如何计算边界最大子数组?既然这种情况下已经确定了包含第二个元素,②那么我们只需分两种情况:只包含第二个元素,和不只包含第二个元素;同样取这两种情况的最大值。只包含第二个元素的情况是非常简单的,边界最大子数组就只是A[2]的值;不只包含第二个元素的情况也简单,不只包含第二个元素,那么必定包含它的前一个元素,即第一个元素,所以我们需要它的前一个元素的边界最大子数组。之后A[2]的边界最大子数组就是这两种情况的最大值。

也就是说,要确定第A[1..2]的最大子数组,唯一另外需要的元素就是第一个元素的边界最大子数组。

现在情况清晰了,当计算A[1..2]的最大子数组是,需要的值分别有:前最大子数组(已知)、A[2]的值(已知)、前一个元素的边界最大子数组。

很明显这是一个从头开始,可以迭代求解的问题,迭代的每一步都只需要上一段中加粗的三个值;每一步都为下一步的计算提供了基础的值。这是一个线性的高效算法。

代码解释

如果看明白了思路,那么代码就很容易解释了。 每一步都根据上一步的边界最大子数组和本次迭代的值求出本次的边界最大子数组,在把本次的边界最大子数组与前最大子数组比较,确定本次的最大子数组。

扩展-记录索引值

这里因为与代码结合着讨论的,所以下标从0开始。 这里再说一下两个自定义名词: 前最大子数组:不包含当前元素的最大子数组 边界最大子数组:只包含当前元素和不只包含当前元素,两种情况的较大值

我们以数组{1,-2,3,10,-4,7,2,-48}为例。 初始时两个索引都为0,最大子数组和边界最大子数组都是1; 当迭代索引为1时,本次值为-2,前一元素的边界最大子数组为1,所以边界最大子数组为-1,前最大子数组为1,本次迭代的最大子数组为前最大子数组,值为1,不更新索引; 当迭代索引为2时,本次值为3,前边界最大子数组为-1,所以边界最大子数组为3;前最大子数组为1,本次迭代的最大子数组为边界最大子数组,值为3;此时需要把起始索引和终止索引都更新为当前索引,即2; 当迭代索引为3是,本次值为10,前边界最大子数组为3,所以边界最大子数组为13,前最大子数组为3,本次迭代的最大子数组为边界最大子数组,值为13;此时需要把终止索引更新为当前索引,却不能更新起始索引; ………… 索引为2和索引为3的共同点在于,都是边界最大子数组大于前最大子数组,都更新了终止索引;差别在于,索引2为时,边界最大子数组只包含了索引对应的值,所以可以更新起始索引;而索引3的边界最大子数组也包含了前一元素,所以只能更新终止索引。

此时可以把需要更新索引的情况概括如下: 条件①:本次的边界最大子数组只包含当前值,且大于前最大子数组,则更新起始索引; 条件②:本次的边界最大子数组大于前最大子数组,则更新终止索引;

更新终止索引的条件②应该是充分且必要的,然而更新起始索引的条件①是充分的,确并不是必要的。考虑一下数组{4,-5,1,5},当索引为2时,边界最大子数组为1,前最大子数组为4,只满足条件1的前半部分,然而整个数组的最大子数组的起始索引却是2。所以条件①需要进行补充。

以下是第二个版本的两个条件: 条件①:本次的边界最大子数组只包含当前值 条件②:本次的边界最大子数组大于前最大子数组 当满足条件①时,把当前索引记录为缓存索引,但并不更新起始索引;当满足条件②时,更新终止索引为当前索引,更新起始索引为缓存索引。 条件②的满足总是要在条件①之后的。条件①可能标志着一个新的开始,因为条件①可以重复满足,而条件②必定标志着一个结束。

思路理清以后,代码就手到擒来了

int *maxSubArray( int* array, int length)
{
    int boundry = array[0];
    int maxArray = array[0];
    int maxEndIndex = 0;
    int maxBeginIndex = 0;
    int tmpBeginIndex = 0;
    for( int i=1; i<length; ++i )
    {
        if( boundry+array[i] >= array[i] ) 
        {
            boundry += array[i];
        }
        else
        {
            boundry = array[i];
            tmpBeginIndex = i;
        }
        if( maxArray < boundry )
        {
            maxArray = boundry; 
            maxEndIndex = i;
            maxBeginIndex = tmpBeginIndex;
        }
    }
    int *result = new int[3];
    result[0] = maxBeginIndex;
    result[1] = maxEndIndex;
    result[2] = maxArray;
    return result;
}

#include<iostream>
using namespace std;
int main()
{
    int a[] = {1,-2,3,10,-4,7,2,-48};
    int num = sizeof(a)/sizeof(a[0]);
    int* result = maxSubArray(a, num);
    cout<<"Begin:"<<result[0]<<"  End:"<<result[1]<<"  Num:"<<result[2]<<endl;

    int b[] = {3,-1,5,-1,9,-20,21,-20,20,21};
    num = sizeof(b)/sizeof(b[0]);
    result = maxSubArray(b, num);
    cout<<"Begin:"<<result[0]<<"  End:"<<result[1]<<"  Num:"<<result[2]<<endl;

    return 0;
}

输出结果为:

Begin:2 End:6 Num:18 
Begin:6 End:9 Num:42

主函数里是有内存泄露,但这并不是重点

猜你喜欢

转载自my.oschina.net/hengbao666/blog/1796206