「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
题目
给你一个整数数组 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
进阶: 如果你已经实现复杂度为O(n)
的解法,尝试使用更为精妙的 分治法 求解。
初看题
直接来个暴力的, 双循环,数组元素一次相加后和当前最大值对比,更新最大值 预料之中的超时
function maxSubArray(nums) {
const n = nums.length
let r = nums[0]
// 第一次循环, 循环到倒数第二个就可以
for(let i = 0; i < n - 1;i++) {
// 定义以第i个元素开始的情况下的最大和
let sum = nums[i]
// 第二层循环
for(let j = i + 1; j < n; j++) {
// 每个已第i个元素开头的子序和都算上和
sum += nums[i]
// 如果大于当前最大值,去更新最大值
if(sum >= r) {
r = sum
}
}
}
return r
}
这种双循环暴力解法超时了,只是讲解下思路: 把每个子序的和都列出来,然后一一比对大小
动态规划
看到连续子序最大和 就可以尝试动态规划去试试
- 思考下求连续子序最大和, 假设我们通过第dp[i] 记录子序列的最大和,会有一个问题就是,在确定子序最大和的时候,假设当前元素的值小于0,我们也没法确定再向后遍历加起来的和是不是大于当前最大和
上面这个列子中, 当dp[2] 的时候我们没法确认结果值, 所以呢// nums [1, 3, -1, 2, -4, 1] // dp [1, 4, 4, 怎么处理]
- 我们定义一个
r
变量代表当前子序最大和,通过dp记录子序和 - 可以思考一下: 如果当前元素值
nums[i]
,大于了dp[i - 1] + nums[i]
, 就代表子序列nums[i]
是当前和最大子序列, 就不需要带上前面的子序列,需要重置下子序,对应到dp就是以nums[i]
的全新子序 - 递归公式可以得到
dp[i] = Math.max(nums[i], dp[i - 1])
function maxSubArray(nums) {
// 1. 定义动态初始状态,和 结果
let dp = [nums[0]], r = nums[0]
for (let i = 1; i < nums.length; i++) {
// 第i项的最大和
dp[i] = Math.max(dp[i - 1] + nums[i], nums[i])
r = Math.max(dp[i], r)
}
return r
}
上面因为遍历nums, 对于dp我们只用到了i 和 i - 1 项, 所以可以用一个变量来代替dp
function maxSubArray(nums) {
// 1. 定义动态初始状态,和 结果
let pre = nums[0], r = nums[0]
for (let i = 1; i < nums.length; i++) {
// 第i项的最大和
pre = Math.max(pre + nums[i], nums[i])
r = Math.max(pre, r)
}
return r
}
贪心
通过动态规划的思路
- 我们通过一个变量
r
记录最大和,只有当前和值是大于等于0的时候相加才有意思 - 当前和如果小于0 则重置为0, 在这个过程中 更新r的值
function maxSubArray(nums) {
let r = -Infinity
let sum = 0
for (let i = 0; i < nums.length; i++) {
sum += nums[i]
r = Math.max(r, sum)
if (sum < 0) {
sum = 0
}
}
return r
}
分治法
先来代码
function minSubArray(nums) {
return getRange(nums, 0, nums.length - 1).mSum
}
function Status(lSum, rSum, iSum, mSum) {
this.lSum = lSum
this.rSum = rSum
this.iSum = iSum
this.mSum = mSum
}
function getRange(nums, start, end) {
if (start === end) {
const sum = nums[start]
return new Status(sum, sum, sum, sum)
}
const mid = (l + r) >> 1
const l = getRange(nums, start, mid)
const r = getRange(nums, mid + 1, r)
const lSum = Math.max(l.lSum, r.iSum + l.lSum)
const rSum = Math.max(r.rSum, l.iSum + r.rSum)
const iSum = l.iSum + r.iSum
const mSum = Math.max(l.mSum, r.mSum, l.rSum + r.lSum)
return new Status(lSum, rSum, iSum, mSum)
}
- 合并两个子序的时候: 需要知道 左侧最大子序和、右侧最大子序和、 横跨左右子序的的和(因为存在和最大分段的情况) 这三个值的最大值就是最大的子序和
- 所以在分治的时候,先知道
最大子序和
,左侧序列最大和
,右侧序列最大和
=> 还有因为有分段的情况,所以 要知道整个子序列的和
- 对
最大子序和
,左侧序列最大和
,右侧序列最大和
进行连续的情况的最大和的对比
总结
这篇真实不容易