算法学习(一)——分治策略之最大子数组问题(Java实现)

在分治策略中,递归地求解一个问题,在每层递归中应用如下三个步骤:

  • 分解步骤将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小。
  • 解决步骤递归地求解出子问题,如果子问题的规模足够小,则停止递归,直接求解。
  • 合并步骤将子问题的解组合成原问题的解。

当子问题足够大,需要递归求解时,称之为递归情况,当子问题变得足够小,不再需要递归时,递归已经“触底”,进入了基本情况。有时,除了与原问题形式完全一样的规模更小的子问题外,还需要求解与原问题不完全一样的子问题,我们将这些子问题的求解看做合并步骤的一部分。
问题描述:在给出的数组A中寻找出A的和最大的非空连续子数组,这个连续子数组我们称之为最大子数组。(A中的数值应当不全是正数,只有数组A中有负数,这个问题才有意义,否则最大子数组就是A自身)
数组A:13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7。
问题分析:我们现在就按照上面说的三个步骤来解决问题,首先我们要分解,这就意味着要将数组分解成两个规模尽量相等的子数组,也就是对半分,找到数组的中央,比如说mid,然后问题就变成求解数组A[low,mid]和A[mid+1,high]的最大子数组,A[low,high]的任意连续子数组A[i,j]所处的位置必然是下面三种情况之一:

  • 完全位于子数组A[low,mid]中,因此low<=i<=j<=mid。
  • 完全位于子数组A[mid+1,high]中,因此mid+1<=i<=j<=high。
  • 跨越了中点,因此low<=i<=mid< j<=high。

因此,A[low,high]的一个最大子数组所处的位置必然是这三种情况之一。实际上我们所求的最大子数组必然是上面三种情况中和最大的那一种,对于完全位于A[low,mid]和A[mid+1,high]中的情况,我们可以采取递归来求解,因为这两个问题仍然是最大子数组问题,只是规模更小了,所以现在的主要工作就是寻找出跨越了中点的最大子数组,然后跟其它两种情况比较,选出和最大者。
寻找跨越了中点的最大子数组只需要两个循环分别从mid向前向后寻找连续的和最大子数组,然后将其合并,该算法的伪代码如下:

FIND-MAX-CROSSING-SUBARRAY(A,low,mid,high)
left-sum = -∞
sum = 0
for i = mid downto low
    sum = sum + A[i]
    if sum > left-sum
        left-sum = sum
        max-left = i
right-sum = -∞
sum = 0
for j = mid + 1 to high
    sum = sum + A[j]
    if sum > right-sum
        right-sum = sum
        max-right = j
return (max-left,max-right,left-sum + right-sum)

该代码以接收数组A和下标low、mid和high为输入,返回一个下标元祖划定跨越中点的最大子数组的边界,并返回最大子数组中值的和。这个过程第一个循环求出从mid开始的左边最大连续子数组,第二个循环求出从mid+1开始的右边最大连续子数组,最后将两者合并。
如果数组A包含n个元素,这个过程花费O(n)的时间,两个循环加在一起等于将数组A遍历了一次,所以总循环迭代次数为n,因此是O(n)。
下面是求解最大子数组问题的分治算法的伪代码:

FIND-MAXIMUM-SUBARRAY(A,low,high)
if high == low
    return (low,high,A[low])
else mid = (low+high)/2
    (left-low,left-high,left-sum) = FIND-MAXIMUM-SUBARRAY(A,low,mid)
    (right-low,right-high,right-sum) = FIND-MAXIMUM-SUBARRAY(A,mid+1,high)
    (cross-low,cross-high,cross-sum) = FIND-MAX-CROSSING-SUBARRAY(A,low,mid,high)
    if left-sum >= right-sum and left-sum >=cross-sum
        return (left-low,left-high,left-sum)
    else if right-sum >=left-sum and right-sum >= cross-sum
        return (right-low,right-high,right-sum)
    else return (cross-low,cross-high,cross-sum)

根据这两个伪代码,我用Java实现了这个求解最大子数组问题,当然也可以根据给出的伪代码用其它语言实现,代码如下:

public class FindMaximumSubArray {

    public static void main(String[] args) {

        int[] a = {13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7};
        ArrayList<Integer> result=findMaximumSubArray(a, 0, a.length-1);
        System.out.println(result.toString());

    }

    /*
     * 求跨越中点的最大子数组,返回包含最大子数组边界以及最大子数组中值的和的集合
     */
    public static ArrayList<Integer> findMaxCrossingSubArray(int[] a, int low,
            int mid, int high) {
        int leftSum = (int) Double.NEGATIVE_INFINITY;
        int sum = 0;
        int maxLeft = low;
        for (int i = mid; i >= low; i--) {
            sum = sum + a[i];
            if (sum > leftSum) {
                leftSum = sum;
                maxLeft = i;
            }
        }
        int rightSum = (int) Double.NEGATIVE_INFINITY;
        sum = 0;
        int maxRight = high;
        for (int i = mid + 1; i <= high; i++) {
            sum = sum + a[i];
            if (sum > rightSum) {
                rightSum = sum;
                maxRight = i;
            }
        }
        ArrayList<Integer> result = new ArrayList<Integer>();
        result.add(maxLeft);
        result.add(maxRight);
        result.add(leftSum + rightSum);
        return result;
    }

    /*
     * 求最大子数组,返回包含最大子数组边界以及最大子数组中值的和的集合
     */
    public static ArrayList<Integer> findMaximumSubArray(int[] a, int low, int high) {
        ArrayList<Integer> result = new ArrayList<Integer>();
        if (low == high) {
            result.add(low);
            result.add(high);
            result.add(a[low]);
            return result;
        } else {
            int mid = 0;
            ArrayList<Integer> leftResult = null;
            ArrayList<Integer> rightResult = null;
            ArrayList<Integer> crossResult = null;
            mid = (low + high) / 2;
            leftResult = findMaximumSubArray(a, low, mid);
            rightResult = findMaximumSubArray(a, mid + 1, high);
            crossResult = findMaxCrossingSubArray(a, low, mid, high);
            if (leftResult.get(leftResult.size() - 1) >= rightResult
                    .get(rightResult.size() - 1)
                    && leftResult.get(leftResult.size() - 1) >= crossResult
                            .get(crossResult.size() - 1)) {
                    return leftResult;
            }else if(rightResult
                    .get(rightResult.size() - 1) >= leftResult.get(leftResult.size() - 1)
                    && rightResult.get(leftResult.size() - 1) >= crossResult
                            .get(crossResult.size() - 1)){
                return rightResult;
            }else{
                return crossResult;
            }
        }
    }
}

结果:
这里写图片描述
可以看到求出的结果,最大子数组是从数组下标为7开始到下标为10结束,计算7-10的和,18+20+(-7)+12=43。
算法时间复杂度分析:如果根据主方法,我们可以很容易就得到该算法的时间复杂度为:
T(n)= O(1),若n=1;T(n)=2T(n/2)+O(n),若n>1。
这里提一下主方法,主方法就是可求解形如下面公式的递归式的界:
T(n) = aT(n/b) + f(n)
其中a >= 1,b > 1,f(n)是一个给定的函数。它刻画了这样一个分治算法:生成a个子问题,每个子问题的规模是原问题规模的1/b,分解和合并步骤总共花费时间为f(n)。
现在来具体分析T(n)是怎么来的,首先,当n=1时,即数组只有一个元素时,则符合第一个判断条件,进入第一个判断的语句块,这里的代码花费常量时间,因此T(n)= O(1)。当n>1的时候,可以看到第一个判断条件依旧花费常量时间,然后主要分析就是左右子数组的递归情况,因为子问题均为n/2个元素的子数组,因此每个子问题的求解时间为T(n/2),由于分解为了2个子问题即左数组和右数组,所以这两个问题的总运行时间为2T(n/2),而对于调用求跨越中点最大子数组方法的时间复杂度我们上面已经求出来是O(n),后面判断最大和仅花费O(1)时间,所以有
T(n) = O(1) + 2T(n/2) + O(n) + O(1) = 2T(n/2)+O(n)
最后有个小练习,当A的所有元素均为负数时,会返回什么,我觉得会返回最大的那个负数,而通过实验发现确实返回了最大的那个负数,并且是返回第一次出现的那个最大负数。

猜你喜欢

转载自blog.csdn.net/luqiren/article/details/76850678