最大子列和问题

今天来讨论一个很基础的算法问题,数列的最大子列和问题。这道题我是在看浙大陈姥姥的Mooc的时候看到的,算是陈越老师作为算法与数据结构开篇讲解的第一道算法实例题。

那么今天我就来记录一下分析这道题的过程。

常用方法

首先,最大子列和这个问题有一个众所周知的办法,即为每次从数列的开头i,往结尾N累加,当加至结尾时,由i+1再次累加,直到N-N。这样的算法用三个变量三层循环来分别代表从头至尾的遍历,以及从i - N的前进继续累加,最后一层是累加的和。算法可以写成下面这样:

	public static int MaxSubseqSum1(int[] A, int N) {
        int ThisSum, MaxSum = 0;
        int i, j, k;
        for (i = 0; i < N; i++) {
            for (j = i; j < N; j++) {
                ThisSum = 0;
                for (k = i; k <= j; k++) {
                    ThisSum += A[k];
                }
                if (ThisSum > MaxSum) {
                    MaxSum = ThisSum;
                }
            }
        }
        return MaxSum;
    }

上面的第一种方法应该非常好理解,而由于是三层循环,所以,这个算法的时间复杂度是T(N) = O(N^3)。这种时间复杂度的算法,是非常的低效的,并且我们作为一个有追求的程序员,看到一个时间复杂度上有平方以上指数的,必须要考虑的是降次。

那么其实,第一种算法,如果我们仔细思考,那么可以发现它最里面的一层,k循环是一个很愚蠢的行为,因为我们可以直接在第二层循环里完成累加,于是,我们可以写一个稍微简单的算法。

	public static int MaxSubseqSum2(int[] A, int N) {
        int ThisSum;
        int MaxSum = 0;
        int i, j;
        for (i = 0; i < N; i++) {
            ThisSum = 0;
            for (j = i; j < N; j++) {
                ThisSum += A[j];
                if (ThisSum > MaxSum) {
                    MaxSum = ThisSum;
                }
            }
        }
        return MaxSum;
    }

而这个去了一个循环的算法,时间复杂度也一目了然,T(N) = O(N^2),但是时间复杂度依旧还有2次方。接下来还有什么更好的办法么?

分治法

在这里我们介绍一种方法叫分治法,分而治之。这个方法的思想是,先把数列切割成左右两个部分,接下来,递归的把数列不断切割为两份,直到最小单位为一个元素。而这时,分别去求他们的子列和,并且在求算左半边和右半边的子列和之后,把跨越二分边界的子列和也求解出来。比较左半边的最大子列和,以及右半边的最大子列和,以及跨越边界的最大子列和。取出最大的那个数,即为整个数列的最大子列和。

这是一种很常用的算法思想,可以先看代码来理解一下。

	/**
     * 分治法,保持API一致
     * @param A 求解数列
     * @param N 元素总数
     * @return
     */
	public static int MaxSubseqSum3(int[] A, int N) {
        return DivideAndConquer(A, 0, N-1);
    }

	/**
     * 分治法主体
     * @param List 求解数列
     * @param left 左半边的下标
     * @param right 右半边的下标
     * @return 所求数列的最大子列和
     */	
    public static int DivideAndConquer(int[] List, int left, int right) {
        int MaxLeftSum, MaxRightSum;
        int MaxLeftBorderSum, MaxRightBorderSum;
        int LeftBorderSum, RightBorderSum;
        int center, i;

        if (left == right) {
            if (List[left] > 0) {
                return List[left];
            } else {
                return 0;
            }
        }

        center = (left + right) / 2;
        
		//分解数列 不断递归调用
        MaxLeftSum = DivideAndConquer(List, left, center);
        MaxRightSum = DivideAndConquer(List, center + 1, right);
		
		//分别结算左右两边的跨越边界的和
        MaxLeftBorderSum = 0; LeftBorderSum = 0;
        for (i = center; i >= left; i--) {
            LeftBorderSum += List[i];
            if (LeftBorderSum > MaxLeftBorderSum) {
                MaxLeftBorderSum = LeftBorderSum;
            }
        }

        MaxRightBorderSum = 0; RightBorderSum = 0;
        for (i = center + 1; i <= right; i++) {
            RightBorderSum += List[i];
            if (RightBorderSum > MaxLeftBorderSum) {
                MaxRightBorderSum = RightBorderSum;
            }
        }
        return Max3(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum);
    }
    
	 /**
     * 取三个数中的最大值
     * @return int
     */
    private static int Max3(int A, int B, int C) {
        return A > B ? A > C ? A : C : B > C ? B : C;
    }

而分治法的时间复杂度,是

T(1) = O(1)
T (N) = 2T(N/2) + cN
= 2 [2T( N/22 ) + cN/2] + cN
= 2kO(1) + ckN 
= O(NlogN )

现在我们可以看到,这个问题我们已经完成我们的降次目标了。那么还有更好的算法么?

在线处理

这个问题有个最简单的算法,叫在线处理法,遍历数列的时候,顺便累加,每次累加的和若是小于0,那么我们可以认为最大子列和为负数时,一定不会让后面的部分增大了,所以就可以把它丢弃,重新置当前的sum = 0

代码如下:

	public static int MaxSubseqSum4(int[] A, int N) {
        int ThisSum, MaxSum;
        int i;
        ThisSum = MaxSum = 0;
        for (i = 0; i < N; i++) {
            ThisSum += A[i];
            if (ThisSum > MaxSum) {
                MaxSum = ThisSum;
            } else if (ThisSum < 0) {
                ThisSum = 0;
            }
        }
        return MaxSum;
    }

在线处理的时间复杂度,因为是只遍历一遍,所以为T(N) = O(N)。 那么说了这么多,我们需要让事实来说话,我们现在准备一个30个元素的队列,让每个算法跑100000次来观察所需时间。

    public static void main(String[] args) {
        int size = 31;
        int[] testArr = {3, -1, 5, 10, -8, 2, 1, 4, 0, 7, -5, -6, 3, -8, -10,
                10, -20, -8, 0, 3, 0, -9, -10, 5, 3, 0, -8, 10, -4, 10, -7};
        int runCount = 100000;
        testFunction1(testArr, size, runCount);
        testFunction2(testArr, size, runCount);
        testFunction3(testArr, size, runCount);
        testFunction4(testArr, size, runCount);
    }

最后的log输出是这样的:

Max Subsequence Sum 1 is 23
function1 run time is  738ms
Max Subsequence Sum 2 is 23
function2 run time is  44ms
Max Subsequence Sum 3 is 17
function3 run time is  110ms
Max Subsequence Sum 4 is 17
function4 run time is  54ms

上面的function的标号,对应上面的4种方法,可以看到,如果我们真的用了第一种笨办法,那么他和高效算法之间的效率差距,实在是太大了。

算法的学习还要继续,多看书,多做题。那么我们下次再分享了。

Search

    Table of Contents