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;
}
附参考资料:
经典算法问题 - 连续子数列的和