题目描述
给你一个整数数组nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
算法思路
1:动态规划:
第一步:对nums数组进行遍历,如果nums[i]前面的子数组和比0小,则无相加必要,直接舍弃,在nums[i]之前的最大数组就是nums[i]本身。否则相加,得出当前到nums[i]为止的最大子数组和。
第二步:每次比较当前最大子序列和与所保存的结果,返回较大的一个。
2:分治法:
将数组分为左中右三段(中间这一段的端点可能横跨左右区间,或者只穿过左区间或右区间),所求子数组一定在三段之内。
3:贪心算法:
从前往后遍历,当出现nums[i]前面的子数组和比0小的情况,视作非最优解,直接舍弃
示例代码(动态规划)
class Solution {
public int maxSubArray(int[] nums) {
//用dp来保存nums的子数组
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1] + nums[i] , nums[i]);
}
int result = dp[0];
for (int i = 1; i < nums.length; i++) {
result = Math.max(result, dp[i]);
}
return result;
}
}
时间复杂度:遍历nums数组,花费O(n)。
空间复杂度:O(n)。
示例代码(贪心)
为降低空间复杂度将dp数组视为一个变量保存,并将两个for循环合成一个。则有:
class Solution {
public int maxSubArray(int[] nums) {
int sum = 0;
int result = dp[0];
for (num : nums) {
sum = Math.max(sum + num , num);
result = Math.max(result, dp[i]);
}
return result;
}
}
时间复杂度:遍历nums数组,花费O(n)。
空间复杂度:O(1)。
示例代码(分治法)
public class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
return maxSubArraySum(nums, 0, len - 1);
}
private int maxCrossingSum(int[] nums, int left, int mid, int right) {
// 一定会包含 nums[mid] 这个元素
int sum = 0;
int leftSum = Integer.MIN_VALUE;
// 左半边包含 nums[mid] 元素,最多可以到什么地方
// 走到最边界,看看最值是什么
// 计算以 mid 结尾的最大的子数组的和
for (int i = mid; i >= left; i--) {
sum += nums[i];
if (sum > leftSum) {
leftSum = sum;
}
}
sum = 0;
int rightSum = Integer.MIN_VALUE;
// 右半边不包含 nums[mid] 元素,最多可以到什么地方
// 计算以 mid+1 开始的最大的子数组的和
for (int i = mid + 1; i <= right; i++) {
sum += nums[i];
if (sum > rightSum) {
rightSum = sum;
}
}
return leftSum + rightSum;
}
private int maxSubArraySum(int[] nums, int left, int right) {
if (left == right) {
return nums[left];
}
int mid = left + (right - left) / 2;
return max3(maxSubArraySum(nums, left, mid),
maxSubArraySum(nums, mid + 1, right),
maxCrossingSum(nums, left, mid, right));
}
private int max3(int num1, int num2, int num3) {
return Math.max(num1, Math.max(num2, num3));
}
}
新知识
本题概念:
1无后效性:为了保证计算子问题能够按照顺序、不重复地进行,动态规划要求已经求解的子问题不受后续阶段的影响。这个条件也被叫做「无后效性」。换言之,动态规划对状态空间的遍历构成一张有向无环图,遍历就是该有向无环图的一个拓扑序。有向无环图中的节点对应问题中的「状态」,图中的边则对应状态之间的「转移」,转移的选取就是动态规划中的「决策」。
2遍历方式归纳:
遍历子串或者子序列的三种遍历方式如下:
第一种:以某个节点为开头的所有子序列: 如 [a],[a, b],[ a, b, c] … 再从以 b 为开头的子序列开始遍历 [b] [b, c]。
第二种:根据子序列的长度为标杆,如先遍历出子序列长度为1的子序列,在遍历出长度为2的等等。
第三种:以子序列的结束节点为基准,先遍历出以某个节点为结束的所有子序列,因为每个节点都可能会是子序列的结束节点,因此要遍历下整个序列,如: 以 b 为结束点的所有子序列: [a , b] [b] 以 c 为结束点的所有子序列: [a, b, c] [b, c] [ c ]。
第一种遍历方式通常用于暴力解法, 第二种遍历方式 leetcode5. 最长回文子串中用到。
第三种遍历方式可以产生递推关系常用于动态规划问题,如背包问题,最大公共子串。这里的动态规划解法也是以先遍历出以某个节点为结束节点的所有子序列的思路。
动态规划为了找到不同子序列之间的递推关系,往往以子序列的结束点为基准。本题的子数列由两部分组成:以前一个位置为结束点的最大子数列、该位置的数值。该算法用到了“最佳子结构”:以每个位置为终点的最大子数列都是基于其前一位置的最大子数列计算得出。可看成动态规划的一个例子。
状态转移方程:sum[i] = max{sum[i-1]+a[i],a[i]} ,其中(sum[i]记录以a[i]为子序列末端的最大序子列连续和)