一、问题理解
问题描述
给定一个整数数组 nums,找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例
text
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6
输入:nums = [1]
输出:1
输入:nums = [5,4,-1,7,8]
输出:23
二、核心思路:前缀和 + 最小前缀和追踪
暴力解法的局限性
枚举所有子数组并计算和,时间复杂度为 O(n²),效率极低。
前缀和优化思路
-
前缀和定义 :
prefix[i]表示从数组开头到第i个元素(不包括第i个)的和 -
核心观察:
-
子数组
nums[i:j]的和 =prefix[j] - prefix[i] -
对于每个位置
j,要找到prefix[j] - prefix[i]的最大值,就是要找到prefix[i]的最小值(其中i < j)
-
-
优化策略:
-
遍历数组,计算前缀和
-
同时维护到当前位置的最小前缀和
-
用当前前缀和减去最小前缀和,得到以当前位置为结尾的最大子数组和
-
更新全局最大和
-
三、代码逐行解析
python
from typing import List
import math
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 1. 初始化
# ans: 存储最大子数组和,初始化为负无穷,确保第一个元素可以更新它
ans = -math.inf
# min_pre_sum: 记录到当前位置的最小前缀和,初始为0(空数组的前缀和)
min_pre_sum = 0
# pre_sum: 当前前缀和,初始为0
pre_sum = 0
# 2. 遍历数组
for x in nums:
# 2.1 更新当前前缀和
pre_sum += x
# 2.2 计算以当前位置为结尾的最大子数组和
# 当前前缀和 - 之前的最小前缀和 = 以当前位置为结尾的最大子数组和
ans = max(ans, pre_sum - min_pre_sum)
# 2.3 更新最小前缀和
# 注意:先计算ans再更新min_pre_sum,确保min_pre_sum是当前位置之前的最小值
min_pre_sum = min(min_pre_sum, pre_sum)
# 3. 返回结果
return ans
四、关键步骤详解
1. 为什么需要 min_pre_sum?
对于任意位置 j,以 j 结尾的最大子数组和为:
text
max_sum_ending_at_j = max(prefix[j] - prefix[i]) for all i < j
由于 prefix[j] 是固定的,要使差值最大,就要使 prefix[i] 最小。因此,我们只需要维护到当前位置的最小前缀和即可。
2. 为什么 min_pre_sum 初始化为 0?
-
空数组的前缀和为 0
-
当数组只有一个元素时,最大子数组和就是该元素本身
-
对于第一个元素
x:pre_sum = x,ans = max(-∞, x - 0) = x -
这样处理确保了单个元素的情况也能正确计算
3. 为什么先计算 ans 再更新 min_pre_sum?
我们需要的是 当前位置之前 的最小前缀和,不包括当前位置。
-
如果先更新
min_pre_sum,可能会用到当前前缀和,这会导致计算出的子数组可能为空 -
先计算
ans确保min_pre_sum是严格在当前位置之前的最小值
五、算法步骤演示
以 nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] 为例:
| 步骤 | x | pre_sum | min_pre_sum | pre_sum - min_pre_sum | ans |
|---|---|---|---|---|---|
| 初始 | - | 0 | 0 | - | -∞ |
| 1 | -2 | -2 | 0 | -2 | max(-∞, -2) = -2 |
| 更新 | - | -2 | min(0, -2) = -2 | - | -2 |
| 2 | 1 | -1 | -2 | 1 | max(-2, 1) = 1 |
| 更新 | - | -1 | min(-2, -1) = -2 | - | 1 |
| 3 | -3 | -4 | -2 | -2 | max(1, -2) = 1 |
| 更新 | - | -4 | min(-2, -4) = -4 | - | 1 |
| 4 | 4 | 0 | -4 | 4 | max(1, 4) = 4 |
| 更新 | - | 0 | min(-4, 0) = -4 | - | 4 |
| 5 | -1 | -1 | -4 | 3 | max(4, 3) = 4 |
| 更新 | - | -1 | min(-4, -1) = -4 | - | 4 |
| 6 | 2 | 1 | -4 | 5 | max(4, 5) = 5 |
| 更新 | - | 1 | min(-4, 1) = -4 | - | 5 |
| 7 | 1 | 2 | -4 | 6 | max(5, 6) = 6 |
| 更新 | - | 2 | min(-4, 2) = -4 | - | 6 |
| 8 | -5 | -3 | -4 | 1 | max(6, 1) = 6 |
| 更新 | - | -3 | min(-4, -3) = -4 | - | 6 |
| 9 | 4 | 1 | -4 | 5 | max(6, 5) = 6 |
最终结果:6
六、与其他解法的对比
1. 动态规划(Kadane算法)
python
def maxSubArray_kadane(nums):
max_sum = nums[0]
current_sum = nums[0]
for i in range(1, len(nums)):
current_sum = max(nums[i], current_sum + nums[i])
max_sum = max(max_sum, current_sum)
return max_sum
复杂度:
-
时间复杂度:O(n)
-
空间复杂度:O(1)
与本题解法的关系:
-
Kadane算法是动态规划的思想:
dp[i]表示以i结尾的最大子数组和 -
本题解法是前缀和的思想:最大子数组和 = 当前前缀和 - 最小前缀和
-
两种方法本质相同,都达到了 O(n) 时间复杂度和 O(1) 空间复杂度
2. 分治法
python
def maxSubArray_divide_conquer(nums):
def helper(left, right):
if left == right:
return nums[left]
mid = (left + right) // 2
# 左半部分最大子数组和
left_max = helper(left, mid)
# 右半部分最大子数组和
right_max = helper(mid + 1, right)
# 跨中点的最大子数组和
left_sum = nums[mid]
left_temp = nums[mid]
for i in range(mid - 1, left - 1, -1):
left_temp += nums[i]
left_sum = max(left_sum, left_temp)
right_sum = nums[mid + 1]
right_temp = nums[mid + 1]
for i in range(mid + 2, right + 1):
right_temp += nums[i]
right_sum = max(right_sum, right_temp)
cross_max = left_sum + right_sum
return max(left_max, right_max, cross_max)
return helper(0, len(nums) - 1)
复杂度:
-
时间复杂度:O(n log n)
-
空间复杂度:O(log n)(递归栈空间)
七、复杂度分析
时间复杂度:O(n)
-
只遍历数组一次
-
每次循环执行常数时间操作(加法、减法、比较)
空间复杂度:O(1)
- 只使用了常数个额外变量(
ans、min_pre_sum、pre_sum)
八、优化技巧与变体
1. 处理全负数数组
python
def maxSubArray_all_negative(nums):
# 如果确定数组全为负数,可以先找到最大值
max_val = max(nums)
if max_val < 0:
return max_val
# 否则使用原算法
ans = -math.inf
min_pre_sum = 0
pre_sum = 0
for x in nums:
pre_sum += x
ans = max(ans, pre_sum - min_pre_sum)
min_pre_sum = min(min_pre_sum, pre_sum)
return ans
2. 返回最大子数组本身
python
def maxSubArray_with_subarray(nums):
ans = -math.inf
min_pre_sum = 0
pre_sum = 0
start = 0
end = 0
temp_start = 0
for i, x in enumerate(nums):
pre_sum += x
if pre_sum - min_pre_sum > ans:
ans = pre_sum - min_pre_sum
start = temp_start
end = i
if pre_sum < min_pre_sum:
min_pre_sum = pre_sum
temp_start = i + 1
return ans, nums[start:end+1]
# 示例
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
max_sum, subarray = maxSubArray_with_subarray(nums)
print(f"最大和: {max_sum}, 子数组: {subarray}")
# 输出: 最大和: 6, 子数组: [4, -1, 2, 1]
3. 处理环形数组(LeetCode 918)
python
def maxSubarraySumCircular(nums):
# 环形数组的最大子数组和有两种情况:
# 1. 最大子数组在数组中间(非环形)
# 2. 最大子数组跨越数组首尾(环形)
# 情况2 = 数组总和 - 最小子数组和
total = sum(nums)
max_sum = nums[0]
min_sum = nums[0]
cur_max = nums[0]
cur_min = nums[0]
for i in range(1, len(nums)):
cur_max = max(nums[i], cur_max + nums[i])
max_sum = max(max_sum, cur_max)
cur_min = min(nums[i], cur_min + nums[i])
min_sum = min(min_sum, cur_min)
# 特殊情况:如果所有数都是负数,则max_sum就是最大负数
if max_sum < 0:
return max_sum
# 环形情况的最大值 = max(非环形最大和, 总和 - 最小和)
return max(max_sum, total - min_sum)
九、常见问题与解答
Q1: 如果数组全为负数,算法还正确吗?
A1: 正确。算法会返回最大的负数。
-
例如
nums = [-2, -1]-
第一步:
pre_sum = -2,ans = -2,min_pre_sum = -2 -
第二步:
pre_sum = -3,ans = max(-2, -3 - (-2)) = -1,min_pre_sum = -3 -
返回
-1,这是正确的(最大子数组是[-1],和为-1)
-
Q2: 为什么 min_pre_sum 初始化为 0 而不是 nums[0]?
A2: 初始化为 0 可以正确处理只有一个元素的情况。
-
如果初始化为
nums[0],对于nums = [1]:pre_sum = 1,ans = 1 - 1 = 0,这是错误的
-
初始化为 0:
pre_sum = 1,ans = 1 - 0 = 1,这是正确的
Q3: 这个算法和 Kadane 算法哪个更好?
A3: 两者时间复杂度都是 O(n),空间复杂度都是 O(1)。
-
Kadane 算法更直观,直接表达了动态规划的思想
-
前缀和算法更数学化,展示了问题本质
-
在实际应用中,Kadane 算法更常用,但两种方法都是正确的
十、相关题目
1. LeetCode 152. 乘积最大子数组
python
def maxProduct(nums):
if not nums:
return 0
max_prod = nums[0]
min_prod = nums[0]
result = nums[0]
for i in range(1, len(nums)):
# 由于负负得正,需要同时维护最大和最小乘积
temp_max = max(nums[i], max_prod * nums[i], min_prod * nums[i])
temp_min = min(nums[i], max_prod * nums[i], min_prod * nums[i])
max_prod, min_prod = temp_max, temp_min
result = max(result, max_prod)
return result
2. LeetCode 918. 环形子数组的最大和(前面已提到)
3. LeetCode 121. 买卖股票的最佳时机
python
def maxProfit(prices):
if not prices:
return 0
min_price = prices[0]
max_profit = 0
for price in prices[1:]:
# 更新最大利润
max_profit = max(max_profit, price - min_price)
# 更新最低价格
min_price = min(min_price, price)
return max_profit
十一、总结
核心要点
-
前缀和是关键:将子数组和问题转化为前缀和之差问题
-
最小前缀和追踪:对于每个位置,最大子数组和 = 当前前缀和 - 之前的最小前缀和
-
时间复杂度优化:从暴力解法的 O(n²) 优化到 O(n)
算法步骤
-
初始化
ans为负无穷,min_pre_sum为 0,pre_sum为 0 -
遍历数组中的每个元素:
-
更新当前前缀和
pre_sum -
更新答案
ans = max(ans, pre_sum - min_pre_sum) -
更新最小前缀和
min_pre_sum = min(min_pre_sum, pre_sum)
-
-
返回
ans
适用场景
-
需要找到连续子数组的最大和
-
数组可能包含负数
-
要求时间复杂度为 O(n)
扩展思考
这个解法展示了如何将看似复杂的问题(最大子数组和)转化为简单的数学关系(前缀和之差),通过追踪最小值来优化计算。这种思想可以应用到许多其他问题中,如:
-
最小子数组和
-
子数组和接近某个目标值
-
二维数组的最大子矩阵和
掌握前缀和+最小前缀和追踪的解法,不仅能够解决最大子数组和问题,还能够解决一系列类似的子数组和问题,是面试中非常重要的算法技巧。