53. 最大子数组和
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入: nums = [1]
输出: 1
示例 3:
输入: nums = [5,4,-1,7,8]
输出: 23
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
解法一:暴力解法
算法步骤:
子序列下标(i,j),初始值i = 0,
1. i不变,j=i,j逐步+1,求出当前i下,所有子序列的和,并找到其中最大值。
2. i+1,重复上述步骤。
3. 全部遍历完成,相当于暴力求和了所有子序列和,最后得到最大值。
小优化点: thisSum在每次j循环之前,置为0。步骤一循环时,例如i=0,j=3时,可以保留(0,3)的和,下一次循环,j+1时,j变为4,求(0,4)的和,直接用上一次(0,3)的和 ,加上 nums[4]即可,不用再重新算一遍0到4的和。
时间复杂度: O(n^2)
实际leetcode解题,用这个解法,数组长度很大的话,时间会超出限制,不能通过。
function maxSubArray(nums: number[]): number {
const len = nums.length;
if(len === 1) return nums[0]
let max = nums[0]
for(let i = 0; i < len; i++){
let thisSum = 0;
for(let j =i; j< len; j++){
thisSum = thisSum + nums[j]
max = Math.max(thisSum, max)
}
}
return max
};
解法二:贪心算法
算法步骤:
从左到右遍历数组,sum用于子序列求和,max用于记录最大值。初始sum为0,max为数组第一个值。
1. 如果 sum > 0,则说明 sum 对结果有增益效果,则sum 保留并加上当前遍历数字
2. 如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
3. 每次比较 sum 和 max的大小,将最大值置为max,遍历结束返回结果
时间复杂度: O(n)
function maxSubArray(nums: number[]): number {
const len = nums.length;
let sum = 0
let max = nums[0]
for(let i =0; i<len; i++){
if(sum>0){
sum = sum + nums[i]
}else{
sum = nums[i]
}
max = Math.max(sum, max)
}
return max
};
解法三:动态规划
把问题转化为:以nums[i]结尾的连续子数组的最大和。相当于倒着遍历。
时间复杂度: O(n)
function maxSubArray(nums: number[]): number {
const len = nums.length;
let pre = 0, max = nums[0];
for(let i = 0; i < len; i++){
pre = Math.max(pre+ nums[i], nums[i])
max = Math.max(max, pre)
}
return max
};
解法四:分治法
算法思路
数组进行分割,平均分成左右两部分。此时有三种情况:
1.最大子序列全部在数组左部分
2.最大子序列全部在数组右部分
3.最大子序列横跨左右数组
对于前两种情况,我们相当于将原问题转化为了规模更小的同样问题。通过再次调用方法,传入起始和结束坐标,递归求解。
对于第三种情况,由于已知循环的起点(即中点),我们只需要进行一次循环,分别找出包含中点左侧第一个元素的左边最大子序列,和包含中点右侧第一个元素的右边的最大子序列。此时序列一定是横跨中点连续的。再将左右最大值相加。
那么,最终计算上面三种情况的最大子序列和, 取出最大的即可。
时间复杂度: O(nlogn)
function maxSubArray(nums: number[]): number {
return maxSubArrayDivideWithBorder(nums, 0, nums.length-1);
};
// 分治法核心代码
function maxSubArrayDivideWithBorder(nums: number[], start: number, end: number): number{
if(start === end) return nums[start]
const mid = Math.floor((start + end)/2)
// 完全在左侧的数组求最大
const leftMax = maxSubArrayDivideWithBorder(nums, start, mid);
// 完全在右侧的数组求最大
const rightMax = maxSubArrayDivideWithBorder(nums, mid+1, end);
// 从分隔位开始,左侧最大和(从中间向左侧连续,至少包含中间这个数)
// 取左侧循环开始的第一个值,初始化为左侧最大值
let midMaxLeft = nums[mid]
let leftSum = 0
for (let i=mid; i >= start; i--){
leftSum = leftSum + nums[i]
if(leftSum>midMaxLeft){
midMaxLeft = leftSum;
}
}
// 从分隔位开始,右侧最大和(从中间向右侧连续,至少包含中间这个数)
// 取右侧循环开始的第一个值,初始化为右侧最大值
let midMaxRight = nums[mid+1];
let rightSum = 0;
for (let i = mid+1; i <= end; i++) {
rightSum = rightSum + nums[i];
if (rightSum > midMaxRight) {
midMaxRight = rightSum;
}
}
// 最大子序列横跨左右数组:中间值连续时的最大和
const midCrossMax = midMaxLeft + midMaxRight;
return Math.max(midCrossMax, Math.max(leftMax, rightMax));
}