文章作者:Tyan
博客:noahsnail.com | CSDN | 简书
本文主要是对最大子数组(序列)问题求解的学习与总结,最大子数组问题是一道经典的算法题,这道题解法有很多,因此可以学习到很多求解问题的思路,并可以学习到算法的优化过程。
1. 问题描述
英文:
Find the contiguous subarray within an array (containing at least one number) which has the largest sum.
For example, given the array [-2,1,-3,4,-1,2,1,-5,4],
the contiguous subarray [4,-1,2,1] has the largest sum = 6.
中文:
主要是给定一个数组,求解数组的子数组中,数组元素和最大的那一个子数组,返回的是最大子数组的和。
2. 求解
解法一
最简单也是最容易想到的思路就是三层循环,对(i,j),i<=j
的情况进行遍历,这种情况下的算法复杂度为O($n^3$)。代码如下:
public class Solution {
public int maxSubArray(int[] nums) {
//如果需要节省空间,可将n替换
int n = nums.length;
int max = nums[0];
for(int i = 0; i < n; i++) {
for(int j = i; j < n; j++){
int sum = 0;
//注意k的边界,存在i=j的情况
for(int k = i; k <= j; k++) {
sum += nums[k];
if(sum > max) {
max = sum;
}
}
}
}
return max;
}
}
Leetcode上的运行结果如下:
解法二
从Leetcode结果可以看出,时间超时了,O($n3$)的时间复杂度确实太高了,需要进行优化。分析上面的代码,在i不变的情况下,j每增加1,其和都是在上次求和基础上加上最新的元素,而在第三层循环中都是重新从i开始计算求和,因此存在数据冗余(求和的重复计算),因此需要需要去掉算法中的冗余部分。这种情况下的代码复杂度变为O($n2$),代码如下:
public class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
int max = nums[0];
for(int i = 0; i < n; i++) {
int sum = 0;
for(int j = i; j < n; j++){
sum += nums[j];
if(sum > max) {
max = sum;
}
}
}
return max;
}
}
Leetcode上运行结果如下:
解法三
从Leetcode结果可以看出,时间还是超时了,但从执行的测试数据数量上来看,比第一次多执行了两个,但在最后一个测试数据上时间超时了。那么能不能有进一步的优化呢?答案是肯定有的。可以使用分治法来求解,算法复杂度为O(nlogn),但是其实本题并不适合使用分治法,太复杂。虽然算法复杂度降低了一些,因此这里略过分治法,直接寻找更优解法。
解法四
还有没有更好的方法呢?答案也是肯定的。首先假设存在最大子数组X,则最大子数组X中的任意一个子数组x都不应该为负数,如果x为负数,则X必定不是最大子数组(可用反证法证明)。根据这个思想,我们只需要以此累加数组元素,并将和与0比较,如果小于0,则需要在剩下的元素中重新寻找是否存在最大子数组,如果不小于0,则与保存的最大子数组值进行比较,如果大于最大子数组值,则更新最大子数组值。这样只需要一次遍历就能找到最大子数组,这种解法的算法复杂度为O(n)。根据这个思路,解决这个问题的算法复杂度代码如下:
public class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
int max = nums[0];
int sum = 0;
for(int i = 0; i < n; i++) {
sum += nums[i];
if(sum > max) {
max = sum;
}
if(sum < 0) {
sum = 0;
}
}
return max;
}
}
Leetcode通过了。
解法五
还有没有别的方法呢?答案还是肯定的。使用动态规划求解。动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。 使用动态规划求解问题,最重要的就是确定动态规划三要素:(1)问题的阶段;(2)每个阶段的状态;(3)从前一个阶段转化到后一个阶段之间的递推关系。
1.起始阶段(i=0)
,max = nums[0]
;2.第i(i > 0)
个阶段,max = curMax[i]
,curMax
是第i个阶段的最大子序列和;3.第i-1
和第i
个阶段的关系,curMax[i] = Math.max(curMax[i - 1] + nums[i], nums[i])
;4.根据前面动态规划的定义,则最大子序列和max = Math.max(max, curMax[i])
。
public class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
//curMax是当前的最大子序列和
int[] curMax = new int[n];
curMax[0] = nums[0];
int max = nums[0];
for (int i = 1; i < n; i ++) {
curMax[i] = Math.max(curMax[i - 1] + nums[i], nums[i]);
max = Math.max(max, curMax[i]);
}
return max;
}
}
Leetcode通过了。
分析解法四与解法五
其实解法四与解法五是一致的,解法四中的sum等于解法五中的curMax[i],解法五中如果curMax[i-1]
小于0,则curMax[i] = nums[i]
,而在解法四中由于第i-1
次时sum=curMax[i-1]
,因此需要将sum重置为0,则sum + nums[i] = nums[i]
,与curMax[i] = nums[i]
是一致的。如果解法五中curMax[i-1]
大于等于0,则curMax[i] = curMax[i - 1] + nums[i]
,此时方法四中sum = sum + nums[i]
。而第i-1
次时,sum = curMax[i - 1]
,两者也是等价的。解法五中的curMax[0]替换为sum,curMax[i]替换为sum,将curMax[i] = Math.max(curMax[i - 1] + nums[i], nums[i]);
变换为sum += nums[i];
和if(sum < 0) { sum = 0; }
,即可将代码从解法五变换为解法四的代码。