LeetCode 热题 HOT 100(普通数组)53. 最大子数组和

69 阅读5分钟

题目简介

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

 

进阶: 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。

解题思路

这道题要求找出一个具有最大和的连续子数组,并返回其最大和。我将从以下几个方面来解决它:

  1. 使用动态规划方法,定义状态和状态转移方程
  2. 使用贪心算法,记录当前子数组和和全局最大和
  3. 分治法作为进阶解法

DP

定义 dp[i] 表示以第 i 个元素结尾的连续子数组的最大和。则状态转移方程为:

dp[i] = max(dp[i-1] + nums[i], nums[i])

意思是:要么将第 i 个元素加入子数组,要么重新开始第一个子数组。

代码实现

func maxSubArray(nums []int) int {
    dp := make([]int, len(nums))
    dp[0] = nums[0]
    maxSum := nums[0]
    for i := 1; i < len(nums); i++ {
        dp[i] = max(nums[i], dp[i-1]+nums[i])
        maxSum = max(maxSum, dp[i])
    }
    return maxSum
}

状态变化表

索引元素值dp[i-1]dp[i] = max(dp[i-1] + nums[i], nums[i])全局最大和
0-2--2-2
11-211
2-31-21
34-244
4-1434
52355
61566
7-5616
84156

贪心算法

用一个变量 currentSum 记录当前子数组的和,用另一个变量 maxSum 记录全局最大和。

  • 如果 currentSum 为负数,那么对后续子数组和的贡献为负,所以重置为当前元素。
  • 否则将当前元素统计到 currentSum 中
  • 每次更新 maxsum 为两个变量中的最大值

代码实现

func maxSubArray(nums []int) int {
    window := nums[0]
    maxSum := nums[0]
    for i := 1; i < len(nums); i++ {
        if window < 0 {
            window = nums[i]
        } else {
            window += nums[i]
        }
        maxSum = max(maxSum, window)
    }
    
    return maxSum
}

状态变化表

索引元素值更新前 currentSum更新后 currentSum更新前 maxSum更新后 maxSum当前最大子数组
0-2--2--2[-2]
11-21-21[1]
2-31-211[1]
34-2414[4]
4-14344[4]
523545[4,-1,2]
615656[4,-1,2,1]
7-56166[4,-1,2,1]
841566[4,-1,2,1]

分治法

type SubArrayResult struct {
    maxSum int      // 子数组的最大和
    leftMaxSum int  // 包含左边界的最大和
    rightMaxSun int // 包含右边界的最大和
    totalSum int    // 整个数组的和
}

func maxSubArray(nums []int) int {
    return divideAndConquer(nums, 0, len(nums)-1)    
}

func divideAndConquer(nums []int, left, right int) int {
    if left == right {
        return nums[left]
    }

    mid := (right-left) / 2 + left 
    leftMax := divideAndConquer(nums, left, mid)
    rightMax := divideAndConquer(nums, mid+1, right)
    crossMax := maxCrossSum(nums, left, mid, right)

    return max3(leftMax, rightMax, crossMax)
}

func maxCrossSum(nums []int, left, mid, right int) int {
    leftSum := 0
    leftMaxSum := nums[mid]
    for i := mid; i >= left; i-- {
        leftSum += nums[i]
        if leftSum > leftMaxSum {
            leftMaxSum = leftSum
        }
    }

    rightSum := 0
    rightMaxSum := nums[mid+1]
    for i := mid+1; i <= right; i++ {
        rightSum += nums[i]
        if rightSum > rightMaxSum {
            rightMaxSum = rightSum
        }
    }
    return leftMaxSum + rightMaxSum
}

func max3(l, r, c int) int {
    return max(max(l, r), c)
} 

func max(a, b int) int {
    if a < b {
        return b
    }
    return a
}

分治优化算法

type SubArrayResult struct {
    maxSum int      // 子数组的最大和
    leftMaxSum int  // 包含左边界的最大和
    rightMaxSum int // 包含右边界的最大和
    totalSum int    // 整个数组的和
}

func maxSubArray(nums []int) int {
    return divideAndConquer(nums, 0, len(nums)-1).maxSum    
}

func divideAndConquer(nums []int, left, right int) SubArrayResult {
    if left == right {
        return SubArrayResult{
            maxSum: nums[left],
            leftMaxSum: nums[left],
            rightMaxSum: nums[left],
            totalSum: nums[left],
        }
    }

    mid := (right-left) / 2 + left 
    leftResult := divideAndConquer(nums, left, mid)
    rightResult := divideAndConquer(nums, mid+1, right)
    
    // 结合左右结果
    result := SubArrayResult{}
    crossSum := leftResult.rightMaxSum + rightResult.leftMaxSum

    // 计算整个数组的和
    result.totalSum = leftResult.totalSum + rightResult.totalSum
    // 计算包含左边界的最大和
    result.leftMaxSum = max(leftResult.leftMaxSum, leftResult.totalSum+rightResult.leftMaxSum)
    // 计算包含右边界的最大和
    result.rightMaxSum = max(rightResult.rightMaxSum, rightResult.totalSum+leftResult.rightMaxSum)
    // 计算整个范围的最大子数组和
    result.maxSum = max3(leftResult.maxSum, rightResult.maxSum, crossSum)
    return result
}

func maxCrossSum(nums []int, left, mid, right int) int {
    leftSum := 0
    leftMaxSum := nums[mid]
    for i := mid; i >= left; i-- {
        leftSum += nums[i]
        if leftSum > leftMaxSum {
            leftMaxSum = leftSum
        }
    }

    rightSum := 0
    rightMaxSum := nums[mid+1]
    for i := mid+1; i <= right; i++ {
        rightSum += nums[i]
        if rightSum > rightMaxSum {
            rightMaxSum = rightSum
        }
    }
    return leftMaxSum + rightMaxSum
}

func max3(l, r, c int) int {
    return max(max(l, r), c)
} 

func max(a, b int) int {
    if a < b {
        return b
    }
    return a
}

考察知识点

这道题主要考察以下知识点:

  1. 动态规划:定义状态和状态转移方程,递推求解。

  2. 贪心算法:在每个阶段做出局部最优决策,最终得到全局最优解。

  3. 分治法:将问题分解为子问题,合并子问题的解获得原问题的解。

  4. 时间复杂度分析

    • 贪心算法:O(n)
    • 动态规划:O(n)
    • 分治法:O(n log n)
  5. 空间复杂度优化

    • 贪心和动态规划可以优化到 O(1) 的空间复杂度
    • 分治法需要 O(log n) 的递归栈空间
  6. 问题分析能力:理解连续子数组的概念,并找出最优解。

  7. 代码设计能力:能够根据不同的算法思想实现代码,并选择最优解法。

这是一个经典的子数组问题,贪心算法(Kadane 算法)是解决此类问题的常用方法。理解这种算法思想对于解决类似的子数组和问题非常有帮助。分治法虽然不是最优解法(时间复杂度为 O(n log n)),但是提供了一种不同的思考角度,展示了算法设计的多样性。