[路飞]_leetcode.53.最大子序和

131 阅读2分钟

「这是我参与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,我们也没法确定再向后遍历加起来的和是不是大于当前最大和
    // nums
    [1, 3, -1, 2, -4, 1]
    // dp
    [1, 4, 4, 怎么处理]
    
    上面这个列子中, 当dp[2] 的时候我们没法确认结果值, 所以呢
  • 我们定义一个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)
}
  1. 合并两个子序的时候: 需要知道 左侧最大子序和、右侧最大子序和、 横跨左右子序的的和(因为存在和最大分段的情况) 这三个值的最大值就是最大的子序和
  2. 所以在分治的时候,先知道 最大子序和,左侧序列最大和右侧序列最大和 => 还有因为有分段的情况,所以 要知道整个子序列的和
  3. 最大子序和,左侧序列最大和右侧序列最大和进行连续的情况的最大和的对比

总结

这篇真实不容易