1. 程式人生 > >最大子陣列的和問題--線性演算法

最大子陣列的和問題--線性演算法

最大子陣列的和問題–線性演算法

計算給定陣列的最大子陣列的和有很多種演算法,最常見的是使用分治的策略,然而此問題用分治卻增加了時間複雜度和程式碼複雜度。有更簡單的演算法,本文就將介紹一個線性時間的迭代演算法。這應該是最高效的解決方法了。
首先程式碼如下:

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

主函式裡是有記憶體洩露,但這並不是重點