题目简介
给你一个整数数组 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) 的解法,尝试使用更为精妙的 分治法 求解。
解题思路
这道题要求找出一个具有最大和的连续子数组,并返回其最大和。我将从以下几个方面来解决它:
- 使用动态规划方法,定义状态和状态转移方程
- 使用贪心算法,记录当前子数组和和全局最大和
- 分治法作为进阶解法
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 |
| 1 | 1 | -2 | 1 | 1 |
| 2 | -3 | 1 | -2 | 1 |
| 3 | 4 | -2 | 4 | 4 |
| 4 | -1 | 4 | 3 | 4 |
| 5 | 2 | 3 | 5 | 5 |
| 6 | 1 | 5 | 6 | 6 |
| 7 | -5 | 6 | 1 | 6 |
| 8 | 4 | 1 | 5 | 6 |
贪心算法
用一个变量 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] |
| 1 | 1 | -2 | 1 | -2 | 1 | [1] |
| 2 | -3 | 1 | -2 | 1 | 1 | [1] |
| 3 | 4 | -2 | 4 | 1 | 4 | [4] |
| 4 | -1 | 4 | 3 | 4 | 4 | [4] |
| 5 | 2 | 3 | 5 | 4 | 5 | [4,-1,2] |
| 6 | 1 | 5 | 6 | 5 | 6 | [4,-1,2,1] |
| 7 | -5 | 6 | 1 | 6 | 6 | [4,-1,2,1] |
| 8 | 4 | 1 | 5 | 6 | 6 | [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
}
考察知识点
这道题主要考察以下知识点:
-
动态规划:定义状态和状态转移方程,递推求解。
-
贪心算法:在每个阶段做出局部最优决策,最终得到全局最优解。
-
分治法:将问题分解为子问题,合并子问题的解获得原问题的解。
-
时间复杂度分析:
- 贪心算法:O(n)
- 动态规划:O(n)
- 分治法:O(n log n)
-
空间复杂度优化:
- 贪心和动态规划可以优化到 O(1) 的空间复杂度
- 分治法需要 O(log n) 的递归栈空间
-
问题分析能力:理解连续子数组的概念,并找出最优解。
-
代码设计能力:能够根据不同的算法思想实现代码,并选择最优解法。
这是一个经典的子数组问题,贪心算法(Kadane 算法)是解决此类问题的常用方法。理解这种算法思想对于解决类似的子数组和问题非常有帮助。分治法虽然不是最优解法(时间复杂度为 O(n log n)),但是提供了一种不同的思考角度,展示了算法设计的多样性。