LeetCode 53. Maximum Subarray

Leetcode : MaxSubArray


题目

给定一个数列,数列中的数字有正有负,求这个数列中,最大的子数列的和。
Example

Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.


思路

这个题其实解法挺多
按时间复杂度从差到优一共有四种:

  • 穷举:O(N^3)
  • 穷举的一个优化方案:O(N^2)
  • 递归:O(N*logN)
  • 动态规划:O(N)

由此可以看出,最优的是 动态规划 的算法,当然这个算法实现起来代码也是最简单的。

有一点需要注意,由于可能数列全部为负,那子数列最大和就是个负值,在初始化max的时候,如果初始化为0那就会出现错误的结果
因此应该初始化为 int max = array[0]



解法

穷举

时间复杂度 O(N^3)

这个解法的思路就是暴力的计算,逐个遍历所有可能的子数列,然后找到最大的和。
前两层循环是遍历的找到自数列的起点指针和终点指针,第三层循环是计算这个自数列的和。因此,这个算法的时间复杂度是O(N^3) 。
由这个思路也可以看到,有很多结果都是重复计算的,因此,该算法还是可以进一步优化的。

下面是代码:

public static int maxSubArray(int[] array){
        long startTime = System.nanoTime();

        int max = array[0];  //注意初始化的值为 array[0]
        int tmp;  // 临时保存子数列的和
        for(int begin=0; begin max ? tmp : max;
            }
        }
        return max;
    }


穷举的一个优化

时间复杂度 O(N^2)

由于第一个穷举法,很多结果我们已经计算过。因此在优化时,考虑利用一下已经计算过的数据。
这是参考的文末链接这个小伙伴的思路,我们创建一个数列sumArr,该数列sumArr[i]保存的是array从0~i加起来的和
也就是sumArr[i] = sum(array[0] ~ array[i])
那么子数列的和就是对sumArr两个节点计算差值: sumSubArray(i,j) = sumArr[i] - sumArr[j-1]

这样只需要遍历两层,找到自数列的起点和终点 i 和 j,那两个节点值相减就是自数列的和,不需要再一次遍历计算,时间复杂度优化到了 O(N^2)

在实际代码实践中,不需要额外新建一个sumArr数组,直接利用array数组即可,这样空间复杂度就优化为了常数值O(1)
而且也不需要单独开一个循环去计算这个sumArr的和,只需要在找自数列和的过程中计算即可,代码更精简。(虽然这样写理解稍微麻烦一些)

下面上代码:

public static int maxSubArray(int[] array){
       int max = array[0];
       int outerPre = 0;
       int innerPre;
       int tmp;

        // 不需要额外一个循环去计算值,只需要边循环边记录即可
        for(int i=0; i max ? tmp : max;
                innerPre = array[j];
            }
        }
        return max;
    }


递归

时间复杂度 O(NlogN)*

可以考虑用递归的思路,进一步优化执行时间。
具体思路是,选定一点把数列一分为二,那么最大自数列的值就是从三个值里面选。
1)左边自数列中的最大值
2)右边子数列中的最大值
3)以选定点开始,向两边相加,和的最大值。

以选定点向两边相加求最大值很简单,只需要O(N)的时间复杂度即可。
而对于 1)和 2)我们将问题分为了两个子问题,交给递归来解决,那么问题就简化了。时间复杂度优化为了 O(N*logN)

写递归基本要注意的,终止条件 和 初始值的设置。

下面上代码:

public static int maxSubArray(int[] arr) {
    return recursion(arr, arr.length/2, 0, arr.length-1);
}

private static int recursion(int[] arr, int base, int leftLimit, int rightLimit){
        // 设置初始值,避免负数时出现错误
        int leftMax = arr[leftLimit];
        int rightMax = arr[rightLimit];
        int max = arr[base];

        if(base-leftLimit <=1 && rightLimit-base <=1){
            max = max > leftMax ? max : leftMax;
            max = max > rightMax ? max : rightMax;
            return max;
        }

        int leftSum = 0;
        for(int i=base; i>=leftLimit; i--){
            leftSum+=arr[i];
            if(leftMax < leftSum){
                leftMax = leftSum;
            }
        }

        int rightSum = 0;
        for(int j=base; j<=rightLimit; j++){
            rightSum +=arr[j];
            if(rightMax < rightSum){
                rightMax = rightSum;
            }
        }

        max = leftMax + rightMax - arr[base];
        int searchLeft = searchCaculate(arr, (leftLimit+base)/2, leftLimit, base-1);
        int searchRight = searchCaculate(arr, (base+rightLimit)/2, base+1, rightLimit);

        max = max > searchLeft ? max : searchLeft;
        max = max > searchRight ? max : searchRight;

        return max;
    }


动态规划

时间复杂度O(N)

假设一个值 dp_n 表示以第n个数结尾的数列的最大连续数列的和
例如数列有10个元素,那么以array[10] 结尾的子数列的最大的和为dp_10

那么,就存在如下递推公式:
dp_n = dp_n-1 > 0 ? dp_n-1 + array[n] : array[n]

那么,我们要求的结果就是 max(dp_m),m属于[1,array.length],也就是说,我们全部计算出以第1个元素~第N个元素结尾的子数列的最大的和,然后比较这些数列里最大的那个,就是要求的结果。这样只需要逐步递推,经过O(N) 的时间复杂度就可以求得结果。

leetcode实际执行结果:beats Java submit 96.89% - 9ms

下面上代码:

public int maxSubArray(int[] nums) {
        int max = nums[0];
        int dp_n = nums[0];
        int tmp;
        for(int i=1; i0 ? dp_n : 0);
            max = dp_n>max ? dp_n : max;
        }
        return max;
    }

附参考资料:
经典算法问题 - 连续子数列的和

你可能感兴趣的:(LeetCode 53. Maximum Subarray)