<數據結構與算法分析>讀書筆記--最大子序列和問題的求解

 

如今咱們將要敘述四個算法來求解早先提出的最大子序列和問題。html

第一個算法,它只是窮舉式地嘗試全部的可能。for循環中的循環變量反映了Java中數組從0開始而不是從1開始這樣一個事實。還有,本算法並不計算實際的子序列;實際的計算還要添加一些額外的代碼。git

    public static int maxSubSum1(int[] a) {
        
        int maxSum = 0;
        
        for(int i = 0;i<a.length;i++)
            for(int j = i;j<a.length;j++) {
                int thisSum = 0;
                
                for(int k = i;k<=j;k++)
                    thisSum +=a[k];
                if(thisSum > maxSum)
                    maxSum = thisSum;
            }
    
       return maxSum;
    }
    

 

該算法確定會正確運行(這用不着花太多時間去證實)。運行時間爲O(N的3次方),這徹底取決這兩行代碼:github

     for(int k = i;k<=j;k++)
                    thisSum +=a[k];

 

它們由一個含於三種嵌套for循環中的O(1)語句組成。算法

下面這行代碼循環大小爲N編程

        for(int i = 0;i<a.length;i++)

 

第二個循環大小爲N-i,它可能要小,但也多是N。咱們必須假設最壞的狀況,而這可能會使得最終的界有些大。第三個循環的大小爲j-i+1咱們也要假設它的大小爲N。所以總數爲數組

O(1.N.N.N)=O(N的3次方)。數據結構

而下面這行代碼,開銷只是O(1)post

 int maxSum = 0;

 

然而,下面這段代碼也只不過總共開銷O(N的2次方),由於它們只是兩層循環內部的簡單表達式測試

        if(thisSum > maxSum)
                    maxSum = thisSum;

 

 

第二個算法顯然是O(N的2次方)優化

 public static int maxSubSum2(int [] a) {
       
       int maxSum = 0;
       
       for(int i = 0;i<a.length;i++) {
           
           int thisSum = 0;
           
           for (int j = 0; j < a.length; j++) {
            
               thisSum += a[j];
               
               if(thisSum > maxSum)
                   maxSum = thisSum;
           }
           
       }
       
       
       return maxSum;
       
   }
    

 

對這個問題有一個遞歸和相對複雜的O(N logN)解法,咱們如今就來描述它。要是真的沒出現O(N)(線性的)解法,這個算法就會是體現遞歸威力的極好的範例。該方法採用一種對它們求解,這是「分」的部分。「治」階段將兩個子問題的解修補到一塊兒並可能再作些少許的附加工做,最後獲得整個問題的解。

在咱們的例子中,最大子序列和可能在三處出現。或者整個出如今輸入數據的左半部,或者整個出如今右半部,或者跨越輸入數據的中部從而位於左右兩半部分之中。前兩種狀況能夠遞歸求解。第三種狀況的最大和能夠經過求出前半部分(包含前半部分最後一個元素)的最大和以及後半部分(包含後半部分第一個元素)的最大和而獲得。此時將這兩個和相加。做爲一個例子,考慮下列輸入,如圖:

 

其中前半部分的最大子序列和爲6(從元素A1到元素A3)然後半部分的最大子序列和爲8(從元素A6到A7)。

前半部分包含其最後一個元素的最大和子序列和是4(從元素A1到元素A4),然後半部分 包含其第一個元素的最大和是7(從元素A5到A7)。所以,橫跨這部分且經過中間的最大和爲4+7=11(從元素A1到A7)。

咱們看到,在造成本例中的最大和子序列的三種方式中,最好的方式是包含兩部分的元素。因而,答案爲11。

 

有必要對算法3的程序進行一些說明。遞歸過程調用的通常形式是傳遞輸入的數組以及左邊界和右邊界,它們界定了數組要被處理的部分。單行驅動程序經過傳遞數組以及邊界0和N-1而將該過程啓動。

 

代碼示例以下:

package cn.simple.example;

public class AlgorithmTestExample {

    public static int maxSubSum1(int[] a) {
        
        int maxSum = 0;
        
        for(int i = 0;i<a.length;i++)
            for(int j = i;j<a.length;j++) {
                int thisSum = 0;
                
                for(int k = i;k<=j;k++)
                    thisSum +=a[k];
                if(thisSum > maxSum)
                    maxSum = thisSum;
            }
    
       return maxSum;
    }


   public static int maxSubSum2(int [] a) {
       
       int maxSum = 0;
       
       for(int i = 0;i<a.length;i++) {
           
           int thisSum = 0;
           
           for (int j = 0; j < a.length; j++) {
            
               thisSum += a[j];
               
               if(thisSum > maxSum)
                   maxSum = thisSum;
           }
           
       }
       
       
       return maxSum;
       
   }
    
   
   private static int maxSumRec(int [] a,int left,int right) {
       
       if(left == right)
           
           if(a[left] > 0)
               return a[left];
           else
               return 0;
       
       
       int center = (left + right)/2;
       int maxLeftSum = maxSumRec(a,left,center);
       int maxRightSum = maxSumRec(a,center+1,right);
       
       int maxLeftBorderSum = 0,leftBorderSum = 0;
       
       for(int i = center;i>=left;i--) {
           leftBorderSum +=a[i];
           if(leftBorderSum > maxLeftBorderSum)
               maxLeftBorderSum = leftBorderSum;           
       }
       
       int maxRightBorderSum = 0,rightBorderSum = 0;
       
       for(int i = center+1;i<=right;i++) {
        rightBorderSum += a[i];
        
        if(rightBorderSum > maxRightBorderSum)
            
            maxRightBorderSum = rightBorderSum;
       }
       
       return max3(maxLeftSum,maxRightSum,maxLeftBorderSum + maxRightBorderSum);
   }
   
   private static int max3(int maxLeftSum, int maxRightSum, int i) {
       
    int max;
    
    if(maxLeftSum > maxRightSum)
        max = maxLeftSum;
    
    else
        max = maxRightSum;
    
    if(i > max)
        
        max = i;
    
    return max;
    
   }


public static int maxSubSum3(int [] a) {
       
       return maxSumRec(a,0,a.length-1);
   }
    
}

 

看這段代碼,以下所示:

 if(left == right)
           
           if(a[left] > 0)
               return a[left];
           else
               return 0;
       

 

若是left==right,那麼只有一個元素,而且當該元素非負時它就是最大子序列。left>right的狀況是不可能出現的,除非N是負數(不過,程序中小的擾動有可能導致這種混亂產生)。

 

下面這兩個遞歸調用,咱們能夠看到,遞歸調用老是對小於原問題的問題進行,不過程序小的擾動有可能破壞這個特性。

       int maxLeftSum = maxSumRec(a,left,center);
       int maxRightSum = maxSumRec(a,center+1,right);

 

咱們再看這下面兩段代碼:

代碼1

    int maxLeftBorderSum = 0,leftBorderSum = 0;
       
       for(int i = center;i>=left;i--) {
           leftBorderSum +=a[i];
           if(leftBorderSum > maxLeftBorderSum)
               maxLeftBorderSum = leftBorderSum;           
       }

 

代碼2

int maxRightBorderSum = 0,rightBorderSum = 0;
       
       for(int i = center+1;i<=right;i++) {
        rightBorderSum += a[i];
        
        if(rightBorderSum > maxRightBorderSum)
            
            maxRightBorderSum = rightBorderSum;
       }

 

這兩段代碼達到中間分界處的兩個最大和的和數。這兩個值的和爲擴展到左右兩部分的最大和。例程max3返回這三個可能的最大和的最大者。

 

顯然,算法3須要比前面兩種算法更多的編程努力。然而,程序短並不總意味着程序好。正如咱們在前面顯示算法運行時間的表中已經看到的(能夠參考這篇文章:<數據結構與算法分析>讀書筆記--要分析的問題),除最小的輸入量外,該算法比前兩個算法明顯要快。

 

算法4:

    public static int maxSubSum4(int [] a) {
        int maxSum = 0,thisSum = 0;
        
        for(int j = 0;j<a.length;j++) {
           thisSum +=a[j];
           
           if(thisSum > maxSum)
               
               maxSum = thisSum;
           else if(thisSum < 0)
               thisSum = 0;
            
        }
        
        return maxSum;
    }

 

不難理解爲何時間的界是正確,可是要明白爲何算法是正確可行的卻須要多加思考。爲了分析緣由,注意,像算法1和算法2同樣,j表明當前序列的終點,而i表明當前序列的起點。碰巧的是,若是咱們不須要知道具體最佳的子序列在哪裏,那麼i的使用能夠從程序上被優化,所以在設計算法的時候假設i是須要的,並且咱們想要改進算法2.一個結論是,若是a[i]是負的,那麼它不可能表明最優序列的起點,由於任何包含a[i]的做爲起點的子序列均可以經過用a[i+1)做起點而獲得改進。相似地,任何負的子序列不多是最優子序列的前綴。若是在內循環中檢測到從a[i]到a[j]的子序列是負的,那麼能夠推動i。關鍵的結論是,咱們不只可以把i推動到i+1,並且實際上還能夠把它一直推動到j+1。爲了看清楚這一點,令p爲i+1和j之間的任一下標。開始於下標p的任意子序列都不大於在下標i開始幷包含從a[i]到a[p-1)的子序列的對應的子序列,由於後面這個子序列不是負的(j是使得從下標i開始其值成爲負值的序列的第一個下標)。所以,把i推動到j+1是沒有風險的:咱們一個最優解也不會錯過。

這個算法是許多聰明算法的典型:運行時間是明顯的。但正確性則不那麼容易看出來。對於這些算法,正式的正確性證實(比上面的分析更正式)幾乎老是被須要的;然而,即便到那時,許多人仍然不信服。此外,許多這類算法須要更有技巧的編程,這致使更長的開發過程。不過當這些算法正常工做時,它們運行得很快,而咱們將它們和一個低效的蠻力算法經過小規模的輸入進行比較能夠測試大部分的程序原理。

該算法的一個附帶的優勢是,它只對數據進行一次掃描,一旦a[i]被讀入並被處理,它就再也不須要被記憶。所以,若是數組在磁盤上或經過互聯網傳送,那麼它就能夠被按順序讀入,在主存中沒必要存儲數組的任何部分。不只如此,在任意時刻,算法都能對它已經讀入的數據給出子序列問題的正確答案。具備這種特性的算法叫作聯機算法。僅須要常量空間並以線性時間運行的聯機算法幾乎是完美的算法。

 

代碼示例地址爲:https://github.com/youcong1996/The-Data-structures-and-algorithms/tree/master/algorithm_analysis

相關文章
相關標籤/搜索