前言
一. 最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
这一题和 摆动序列 还挺像的。输出实例如下:
- 输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
- 输出:6
- 解释:连续子数组 [4,-1,2,1] 的和最大,为 6
再讲贪心算法之前,我们先来看下暴力算法,很简单,两个for
循环进行遍历,求每一个组合的总和:
public int maxSubArray(int[] nums) {
int res = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
int count = 0;
for (int j = i; j < nums.length; j++) {
count += nums[j];
res = Math.max(res, count);
}
}
return res;
}
接下来再看下贪心的做法。
1.1 贪心
首先说下上面暴力的一个缺点。我们以nums = [-2,1,-3,4,-1,2,1,-5,4]为例。当我们从第一个元素开始遍历的时候,发现他是个负数。那么问题来了:
- 以当前元素为第首元素,继续往后遍历:-2 + 1 = -1。
- 舍弃掉当前元素:取第二个元素往后遍历: 0 + 1 = 1。
无疑,我们肯定会选择第二种情况,因为两种情况比较下来,无论你想加多少个元素,前者永远比后者要小2。
那么在暴力算法的基础上,我们可以做出如下优化:
- 如果当前和已经小于负数了,这一部分就可以剔除了(贪心的体现),后面的遍历也可以跳过了(指的是第二层循环)。
- 那么就可以直接进入第一层循环的的下一层,那么其实只用一层循环即可。
优化后的贪心算法如下:
public int maxSubArray(int[] nums) {
int res = Integer.MIN_VALUE;
int count = 0;
for (int i = 0; i < nums.length; i++) {
count += nums[i];
res = Math.max(res, count);
// 和小于0了,前面一部分就不要了,重新开始计算子数组和
if (count <= 0) {
count = 0;
}
}
return res;
}
1.2 动态规划
动态规划,我们就需要规定一个状态表达含义以及规划一个状态转移的公式。比如 dp[i]
是在 dp[i-1]
的基础上得到的。针对本题:
- 我们设置
dp[i]
代表:以num[i]
为结尾的最大子数组和。 - 动态转移公式:
dp[i] = Math.max(dp[i-1] + num[i] , num[i])
,因为之前的和可能还是负数。
那么代码就不难写出来:
public int maxSubArray(int[] nums) {
if (nums.length < 2) {
return nums[0];
}
// 初始化dp数组
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = nums[0];
// 遍历数组,dp[0]就是第一个数字它本身
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
res = Math.max(res, dp[i]);
}
return res;
}
我们在优化下代码,观察我们的动态规划公式,可以发现,dp[i]
只和dp[i-1]
有关(仅限于动态规划数组里面的变量),因此我们可以用两个变量来替代dp
数组:用res
代表结果以及dp[i]
,用pre
代表dp[i-1]
:
public int maxSubArray(int[] nums) {
int res = nums[0], pre = 0;
for (int num : nums) {
pre = Math.max(pre + num, num);
res = Math.max(res, pre);
}
return res;
}