1. 程式人生 > >最大子列和問題(JAVA)

最大子列和問題(JAVA)

最大子列和

問題描述:給定N個整數的序列{A1,A2,A3,……,An},求解子列和中最大的值。

這裡我們給出{-2,11,-4,13,-5,-2}這樣一個序列,正確的最大子列和為20

該題是在資料結構與演算法中經常用於分析時間複雜度的典型題目,以下將給出四種方法來求解

一、三層迴圈——窮舉法

(時間複雜度O(N^3))
這種方法的思路就是將每一個子列和都求出來,然後找出最大子列和

法1示例

public static int maxSubSum1(int[] arr){
        //設定整體最大值
        int maxSum = 0;
        //窮舉法遍歷
//迴圈大小:N for( int i = 0;i<arr.length;i++ ){ //迴圈大小:N-i for(int j = i;j<arr.length;j++){ //設定當前最大值 int thisSum = 0; //求解arr[i]~arr[j]的最大值 //迴圈大小:j-i+1 for(int k = i;k<=j;k++){ thisSum += arr[k]; } //更新最大值
if(thisSum > maxSum){ maxSum = thisSum; } } } return maxSum; }

這種做法包含了三個迴圈,第一個迴圈的大小為N,第二個迴圈的大小為N-i,第三個迴圈的大小為j-i+1,根據時間複雜度的計算方法我們這裡都可以看成N,所以時間複雜度為O(n^3)

二、兩層迴圈——窮舉法優化

(時間複雜度O(N^2))

這種演算法是對以一種情況的優化,但是仍然屬於窮舉法,示例圖和第一種方法一樣,這裡不再重複給出。

    public static int maxSubSum2(int[] arr){
        //設定整體最大值
        int maxSum = 0;
        //窮舉法遍歷
        //迴圈大小N
        for(int i = 0;i < arr.length;i++){
            int thisSum = 0;
            //迴圈大小N-i
            for(int j = i;j<arr.length;j++){
                //直接進行累加,改進不必要的第三重迴圈
                thisSum += arr[j];

                if( thisSum > maxSum ){
                    maxSum = thisSum;
                }
            }
        }
        return maxSum;
    }

觀察第一種方法我們可以發現,第三層迴圈雖然儘可能優的列出了所有的情況,但是這種做法是可以進行優化的,按照問題描述,如果我們去除第三重迴圈,仍然可以實現窮舉所有的情況,演算法的時間複雜度變為O(n^2)

三、分治思想——遞迴法

(時間複雜度O(N·logN))

分治思想(divide-and-conquer)的基本思路是:把問題分成兩個大致相等的子問題,然後遞迴地對它們求解,這是“分”的思想;“治”階段將兩個子問題的解修補到一起並可能再做些少許的附加工作,最後得到整個問題的解。

通過這種思想,最大子列和有可能出現在三個地方,左半段、右半段、交界處。
前兩種情況可以通過遞迴求解,而第三種情況需要求出左半段帶邊界(即含有最後一個元素)的最大和以及右半段帶邊界(即含有第一個元素)的最大和而得到。

本題中,將序列從中間分開,分別求兩部分的最大值。由此可見左邊最大的為11,右邊最大的為13,而交界處最大和需要通過左半段帶邊界的最大和,與右半段帶邊界的最大和,求和。(此處不易理解,讀者可以根據圖示中的綠色部分輔助理解,圖中的綠色部分就是符合交界處序列和的值,左邊最大值為7,右邊最大值為13,和為20),綜上比較,該序列的最大序列和為20

法3示例

    public static int maxSubSum3(int[] arr){
        //將陣列arr,陣列的下限0,陣列的上限arr.length-1傳入遞迴函式
        return maxSubSumRec(arr,0,arr.length-1);
    }
    //遞迴函式
    private static int maxSubSumRec(int[] arr,int left,int right){
        //當 left==right 時說明只有一個元素,並且當該元素非負時就是他的最大子序列
        if(left == right){
            if(arr[left] > 0){
                return arr[left];
            }else {
                return 0;
            }
        }
        //遞迴定義部分
        int center = ( left + right) / 2;
        int maxLeftSum = maxSubSumRec(arr,left,center);
        int maxRightSum = maxSubSumRec(arr,center+1,right);

        //遞迴實現部分
        int maxLeftBorderSum = 0;
        int leftBorderSum = 0;
        for(int i = center;i>=left;i--){
            leftBorderSum += arr[i];
            if(leftBorderSum > maxLeftBorderSum){
                maxLeftBorderSum = leftBorderSum;
            }
        }

        int maxRightBorderSum = 0;
        int rightBorderSum = 0;
        for(int i = center + 1;i<=right;i++){
            rightBorderSum += arr[i];
            if(rightBorderSum > maxRightBorderSum){
                maxRightBorderSum = rightBorderSum;
            }
        }
        //判斷三者大小
        return max3(maxLeftSum,maxRightSum,maxLeftBorderSum + maxRightBorderSum);


    }
    //比較函式
    public static int max3(int a,int b,int c){
        int max;
        if(a>b){
            max = a;
        }else {
            max = b;
        }
        if(max>c){
            return max;
        }else {
            return c;
        }
    }

注:我們需要儲存的不僅是左右兩側的最大序列和,還需要得到左右兩側帶邊界的最大序列和,這也就是為什麼左側迴圈從center往前遍歷,而右側迴圈從center+1往後遍歷的原因,此順序不可顛倒

這種方法乍看寫了三個函式,實現該演算法也需要更多的程式設計努力,但實際上程式程式碼量的多少與程式的效率根本並沒有什麼必然的聯絡。

該演算法的時間複雜度為時間複雜度O(N·logN),效率明顯高於前兩種

四、一層迴圈——累計遍歷法

(時間複雜度O(N))

這種方法,將迴圈減為一層,每次累加,並判斷當前最大值(thisSum)與標準最大值(maxSum)的關係。

法4示例

雖然整體只進行了一趟迴圈,但是便於理解,在這裡分成了6部分,每次更新thisSum的值並與maxSum比較。

    public static int maxSubSum4(int[] arr){
        int maxSum = 0;
        int thisSum = 0;

            for(int j = 0;j<arr.length;j++){
            thisSum += arr[j];

            if(thisSum > maxSum){
                maxSum = thisSum;
            }else if(thisSum < 0){
                thisSum = 0;
            }
        }
        return  maxSum;
    }

這種演算法的效率是非常明顯的,時間複雜度為O(N),這種方法可以算是近乎完美的一種求解最大子列和的演算法。根據總結:任何負的子序列都不可能是最有子序列的字首,所以我們可以知道,只要是首位元素為負數的肯定不是最大子列的組成部分。即如果某個子列的首位為負數,那麼它一定要藉助後面的非負數改進。
而且這種演算法還有一種優點就是,它只對資料掃描一次,在任何時刻,演算法都可以對它已經讀入的資料給出正確的答案(具備這種特性的演算法叫做聯機演算法)

下面是四種演算法的在不同N情況下的時間複雜度情況:
演算法時間複雜度
通過觀察我們可以發現,在小量輸入的情況下,幾種演算法的複雜度基本上都是眨眼之間能夠完成的。但是在大量資料的情況下,我們可以看出,後兩中演算法仍然是能夠在短時間內高效的完成同一件事,而前兩種卻顯得異常乏力。

題外話:之前在暑假期間,利用C語言,完成了基本的資料結構演算法部落格的書寫,雖然短時間內能夠記住相關的演算法,但是幾個月來不經常接觸,導致水平仍是停滯不前。鑑於現正在自學java,所以我將通過java來繼續學習資料結構與演算法這方面的知識。一方面可以通過撰文的形式加深記憶,另一方面,由於下學期學校會組織參加ACM大賽、數學建模大賽等比賽,一定要抽出足夠的時間來學習演算法。

才疏學淺,尚有諸多不懂之處,文章多有疏漏,還望前輩斧正。