53. 最大子序和
题目:
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
题解1:暴力法,两层for循环查找最大子序列
主要思路:
- 外层 for 循环控制以 nums[i] 为起始元素的最大子序列
代码:
class Solution {
public int maxSubArray(int[] nums) {
int max = Integer.MIN_VALUE;
int sum = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
sum = nums[i];
max = Math.max(max, sum);
for (int j = i + 1; j < nums.length; j++) {
sum = sum + nums[j];
max = Math.max(max, sum);
}
}
return max;
}
}
复杂度分析:
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
题解2:动态规划法
主要思路:
- 状态数组
dp[i]
,表示以下标 i 结尾元素的最大子序列和 - 最终需要的最大子序列为 dp[i] (i=0,…,n-1)中最大值
- 由 dp[i-1] -> dp[i] , dp[i]的可能值为 dp[i-1]+a[i] 或 a[i] , 较大者为 dp[i] 的值
- 其递推公式为 :
dp[i]= max{dp[i-1]+a[i],a[i]}
- 由此,遍历一次数组即可,其时间复杂度为 O(n)
要点:
- 定义状态数组:
dp[i]
,表示以下标i
结尾元素的最大子序列和 - 递推公式:
dp[i]= max{dp[i-1]+a[i],a[i]}
注意:
pre和 max 初始化为 nums[0]
代码:
class Solution {
public int maxSubArray(int[] nums) {
int max = nums[0];
int pre = nums[0];//此为dp[i-1]
for (int i = 1; i < nums.length; i++) {
pre = Math.max(pre + nums[i], nums[i]);//递推求dp[i]
max = Math.max(pre, max);//递推过程中,保存最大的dp即可
}
return max;
}
}
复杂度分析:
- 时间复杂度:O(N)
- 空间复杂度:O(1)
题解3:贪心法
主要思路:
- 一个个数字加过去,如果
sum<0
,从新的位置开始重新找
主要流程:
- 从左向右遍历数组,保存 sum 与 max 两个变量,
- 每次先对sum进行累加求和,如果sum<0,此时前面的序列和对求最大无增益, 于是重置sum为0,从下个元素重新开始求和
- 之后令 max等于 max与sum的大者
代码:
class Solution {
public int maxSubArray(int[] nums) {
int max = Integer.MIN_VALUE;
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum+=nums[i];
max=Math.max(sum,max);
if(sum<0){
sum=0;//小于0,则从新位置开始重新搜索
}
}
return max;
}
}
复杂度分析:
- 时间复杂度:O(N)
- 空间复杂度:O(1)
题解4:分治法(递归)
主要流程:
- 将序列从中间分开,可以分为左序列,右序列,要找的最大子序列可能存在于左序列,右序列或者横跨左右序列的一个序列(即包含中点 mid,再用贪心法向两边搜索)
- 边界条件为左端点==右端点,此时返回左端点即可
要点:
- 递归形式:每次将序列分为左右序列,最大子序列和为,左序列最大和、右序列最大和、横跨左右序列的最大和,三者最大值
- 递归边界:左端点==右端点
代码:
class Solution {
public int maxSubArray(int[] nums) {
if (nums.length == 0) {
return 0;
}
return maxSub(nums, 0, nums.length - 1);
}
//寻找横跨中间点的最大子序列和,一定包含nums[mid]
public int findCrossingMax(int[] nums, int left, int right, int mid) {
int sum = 0;
int leftMax = Integer.MIN_VALUE;
int rightMax = Integer.MIN_VALUE;
//从中间向左端点找最大序列和
for (int i = mid; i >= left; i--) {
sum += nums[i];
leftMax = Math.max(sum, leftMax);
}
//从中间向右端点找最大序列和
sum = 0;
for (int i = mid + 1; i <= right; i++) {
sum += nums[i];
rightMax = Math.max(rightMax, sum);
}
//由于一定包含mid,所以判断右边是否大于0
if (rightMax > 0) {
return leftMax + rightMax;
} else {
return leftMax;
}
}
//递归函数,寻找最大子序列
public int maxSub(int[] nums, int left, int right) {
//边界判断
if (left == right) {
return nums[left];
}
int mid = left + (right - left) / 2;//获取中点
int leftMax = maxSub(nums, left, mid);//左序列区间最大
int rightMax = maxSub(nums, mid + 1, right);//右序列区间最大
int midMax = findCrossingMax(nums, left, right, mid);//横跨序列区间最大
//三者取最大
return Math.max(leftMax, Math.max(rightMax, midMax));
}
}
复杂度分析:
- 时间复杂度:O(N \log N)O(NlogN),这里递归的深度是对数级别的,每一层需要遍历一遍数组(或者数组的一半、四分之一)。
- 空间复杂度:O(\log N)O(logN),需要常数个变量用于选取最大值,需要使用的空间取决于递归栈的深度。
参考题解:
分治法与动态规划法小总结
分治递归法
编写主要难点在于找到:
- 递归形式
- 递归边界
如本题的递归形式为,求三者最大的子序列,递归边界为左端点等于右端点
具体还可以参考:如何编写递归程序(分治法)
动态规划法
编写主要难点在于:
- 定义状态数组 dp[i]
- 根据状态定义写出递推公式(状态转移方程)
本题中定义的状态数组为 以下标 i 结尾的最大子序列和,递推公式为 dp[i]= max{dp[i-1]+a[i],a[i]}