最大连续子序列求和详解

Maximum Continuous Subsequence Sum

最大连续子序列求和详解

1.        问题描述

输入一个整数序列(浮点数序列也适合本处讲的算法),求出其中连续子序列求和的最大值。

 

2.        算法分析

2.1.        算法一

2.1.1.       算法描述

遍历所有子序列并求和,比较得出其中的最大值。

2.1.2.       代码描述

1          public static int maxSubSumCubic(int[] array) {

2                 int maxSum = 0;  //最大子序列求和

3                 //start表示要求和的子序列的开始索引,end表示结束索引

4                 for(int start = 0; start < array.length; start++) {

5                        for(int end = start; end < array.length; end++) {

6                               int thisSum = 0;   //当前子序列求和

7                               //求出array[start]~array[end]子序列的和

8                               for(int index = start; index <= end; index++) {

9                                      thisSum += array[index];

10                           }

11                           //判断是否大于之前得到的最大子序列求和

12                           if(thisSum > maxSum) {

13                                  maxSum = thisSum;

14                           }

15                    }

16             }

17             return maxSum;

18      }

2.1.3.       算法分析

设输入序列长度为N,算法一有三个循环嵌套,第4行的循环长度为N。第5行的循环长度为N-start+1,因为我们考虑的是最差性能,所以可取为最大的N。第8行的循环长度是end-start+1,同理可取为N。所以可得算法的运行时间是O(N*N*N)=O(N^3 ),即算法的运行时间是以输入长度的立方增长的。可想而知,一旦输入长度变大,算法的运行效率将慢得无法接受,这也从反面说明了算法设计的重要性。

 

2.2.        算法二

2.2.1.       算法描述

算法设计的一个重要原则就是“不要重复做事”。在算法一中,对array[start]~array[end]子序列求和,可以由上一次求和array[start]~array[end-1]的结果加上array[end]得到,而不用从头开始计算。

2.2.2.       代码描述

1          public static int maxSubSumQuadratic(int[] array) {

2                 int maxSum = 0;  //最大子序列求和

3                 //start表示要求和的子序列的开始索引,end表示结束索引

4                 for(int start = 0; start < array.length; start++) {

5                        int thisSum = 0;   //当前子序列求和

6                        for(int end = start; end < array.length; end++) {                            

7                   //已求得的array[start]~array[end-1]子序列的和加上array[end]

8                               //得到array[start]~array[end]子序列的和

9                               thisSum += array[end];                         

10                           //判断是否大于之前得到的最大子序列求和

11                           if(thisSum > maxSum) {

12                                  maxSum = thisSum;

13                           }

14                    }

15             }

16             return maxSum;

17      }

2.2.3.       算法分析

算法二比算法一少了一个循环,同之前的分析一样,容易得到该算法的运行时间为O(N^2 ),算法的运行时间是以输入长度的立方增长的。

 

2.3.        算法三

2.3.1.       算法描述

考虑把输入序列从中间分成两半,那么最大和子序列的位置存在三种情况:1、最大和子序列完全在输入序列的左半部分;2、最大和子序列完全在输入序列的右半部分;3、最大和子序列跨越左右两部分。

 

所以,为了得到输入序列的最大子序列和,我们可以分别求出左半部分的最大子序列和、右半部分的最大子序列和、以及跨越左右两部分的最大子序列和,比较三者得出最大者就是要求的。

求左半部分的最大子序列和,可把左半部分作为新的输入序列通过该算法递归求出。右半部分的最大子序列和也同理。

接下来就是求解跨越左右两部分的最大子序列和,也就是求出左半部分中包含最右边元素(如图中的12)的子序列的最大和,和右半部分中包含最左边(如图中的6)的子序列的最大和,将两者相加即为跨越左右两个部分的最大子序列和。

另外还有一个需要说明的就是,对于有奇数个元素的数组,那么左右两半部分并不是平分的,但这其实不是问题。上面的算法并不要求是两半部分,分成任意两部分都可以。

2.3.2.       代码描述

1          public static int maxSubSumRec(int[] array, int left, int right) {

2                 //递归的基准情况:待处理序列只有一个元素

3                 if(left == right) {

4                        //空集也算是子序列,空集和为0,所以最大子序列和最小为0

5                        if(array[left] > 0)

6                               return array[left];

7                        else

8                               return 0;

9                 }

10            

11             //递归求出左半部分和右半部分的最大子序列和

12             int center = (left + right) / 2;

13             int maxLeftSum = maxSubSumRec(array, left, center);

14             int maxRightSum = maxSubSumRec(array, center + 1, right);

15            

16             //求出左半部分中包含最右边元素的子序列的最大和             

17             int maxLeftBorderSum = 0, leftBorderSum = 0;

18             for(int i = center; i >= left; i--) {

19                    leftBorderSum += array[i];

20                    if(leftBorderSum > maxLeftBorderSum) {

21                           maxLeftBorderSum = leftBorderSum;

22                    }

23             }

24            

25             //求出右半部分中包含最左边元素的子序列的最大和             

26             int maxRightBorderSum = 0, rightBorderSum = 0;

27             for(int i = center + 1; i <= right; i++) {

28                    rightBorderSum += array[i];

29                    if(rightBorderSum > maxRightBorderSum) {

30                           maxRightBorderSum = rightBorderSum;

31                    }

32             }

33            

34             //跨越两个部分的最大子序列和

35             int maxLeftRightSum = maxLeftBorderSum + maxRightBorderSum;

36            

37             //maxLeftSummaxRightSummaxLeftRightSum中的最大值即为最大子序列和

38             int maxSubSum = 0;

39             maxSubSum = maxLeftSum > maxRightSum ? maxLeftSum: maxRightSum;

40             maxSubSum = maxSubSum > maxLeftRightSum ? maxSubSum: maxLeftRightSum;

41            

42             return maxSubSum;

43      }

2.3.3.       算法分析

T(N)表示输入序列长度为N时的运行时间,若N=1,即只有一个元素,那么left==right,所以有T(1)=1。对于N>1,程序需运行两个递归调用和两个for循环。其中每个递归调用的运行时间相当于输入长度为N/2时算法的运行时间T(N/2),两个递归则为2T(N/2)。每个for循环的运行次数为N/2,循环体中的语句运行时间是常数,所以两个for循环的运行时间为O(N/2*2)=O(N)。所以对于N>1,有T(N)=2T(N/2)+O(N),为了方便,可用N代替O(N),数量级不变,所以T(N)=2T(N/2)+N。在Weiss的《数据结构与算法分析Java语言描述(第二版)》中,作者通过观察的方式得出T(N)=N*(K+1),其中N=2^k ,所以有T(N)=N*(k +1)=NlogN +N=O(N )

这里笔者自己尝试推导出这个结果,如有疏误或更好的方法,请不吝指教。同样,假设N2K次方,如果不是2K次方,直接推导可能会比较复杂。当然我们可以这么理解,如果N不是2K次方,可以通过在输入序列开头加入n0,使N=N+n变成2K次方。因为增加了输入的长度,所以运行时间比原本的要长,因为算法分析是最差时的性能,所以能用变长了的运行时间来代替原来的运行时间。所以问题同样转化成N2K次方时,T(N)的表示式怎么求。好了,废话不多说,直接推导。

T(N)=2T(N/2)+N两边同除以N,得

(T(N))/N=(T(N/2))/(N/2)+1   =>  (T(N))/N-(T(N/2))/(N/2)=1

令F(N)=(T(N))/N ,则有

F(N)-F(N/2)=1

往下递推有

F(N/2)-F(N/4)=1

    ...

    F(2) - F(1)=1

以上K(=logN )个式子相加可得

F(N)-F(1)=1+...+1=logN

T(N)/N-T(1)/1=logN

可得

T(N)=N logN+N=O(N logN)

由以上推理过程可得该算法的运行时间为

T(N)=O(N logN) 

2.4.        算法四

2.4.1.       算法描述

设输入序列为A,长度为N,从a0开始求和并记录最大值,如a(0)a(0)+a(1)a(0)+a(1)+a(2)…,直到开始出现求和小于0则停止。设加到a(i)时开始小于0,即有a(0)a(0)+a(1)a(0)+…+a(p-1)都大于0,而a(0)+a(1)+…+a(p)<0。此时,可从a(p+1)重新开始求和并记录最大值。为什么可以这么做呢?我们把从a(1)a(p)之间开始的子序列分为两种情形(设子序列的开始索引为start,结束索引为end):

1end<=pa(start)+…+a(end)=a(0)+…+a(start)+…a(end) –[a(0)+…+a(start-1)]。由前面知,对于start-1<p,有a(0)+…+a(start-1)>0,所以可得到a(0)+…+a(start)+…a(end)> a(start)+…+a(end)。又由于a(0)+…+a(start)+…a(end)已经考虑过了,所以比其小的子序列无需考虑。

2end>p,因为1<=start<=p,有a(0)+…+a(start-1)>0a(0)+…+a(start)+…a(p)<0,所以有a(start)+…a(p)= a(0)+…+a(start)+…a(p)-[ a(0)+…+a(start-1)]<0。对于end>p,有a(start)+…+a(p)+…+a(end)<a(p+1)+…a(end)

综上所述,只需要从a(p+1)开始重新求和,重复以上步骤即可得到最大子序列求和。

2.4.2.       代码描述

1          public static int maxSubSumLinear(int[] array) {

2                 int maxSum = 0, thisSum = 0;

3                 for(int j = 0; j < array.length; j++) {

4                        thisSum += array[j];

5                        if (thisSum < 0) {

6                               thisSum = 0;

7                        }

8                        else if(thisSum > maxSum) {

9                               maxSum = thisSum;

10                    }

11             }

12             return maxSum;

13      }

2.4.3.       算法分析

算法四只有一个for循环,且循环里面语句的执行时间为常量,所以T(N)=O(N)。可见该算法的运行时间是线性增长的,比前三个算法的性能要好。另外一个优点就在于该算法每次就需要使用一个输入值array[j],所以如果是在硬盘或者网络传输的环境下,我们可以实现串行读取,减少内存占用。

 

 

References

《数据结构与算法分析Java语言描述(第2版),Mark Allen Weiss

展开阅读全文

没有更多推荐了,返回首页