LeetCode 53. 最大子数组和
问题描述
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组至少包含一个元素),返回其最大和。
示例:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6。
算法思路
动态规划(DP):
- 状态定义:
dp[i]
表示以nums[i]
结尾的连续子数组的最大和。
- 状态转移:
- 对于当前元素
nums[i]
,有两种选择:- 将
nums[i]
加入前一个子数组(dp[i-1] + nums[i]
)。 - 以
nums[i]
作为新子数组的起点(nums[i]
)。
- 将
- 状态转移方程:
dp[i] = max(nums[i], dp[i-1] + nums[i])
- 对于当前元素
- 结果更新:
- 遍历过程中记录全局最大值
maxSum
。
- 遍历过程中记录全局最大值
空间优化:
- 由于
dp[i]
仅依赖于dp[i-1]
,可用单个变量currentSum
替代 DP 数组。
代码实现
方法一:动态规划(DP 数组)
class Solution {
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
// dp[i] 表示以 nums[i] 结尾的连续子数组的最大和
int[] dp = new int[n];
dp[0] = nums[0]; // 初始化第一个元素
int maxSum = dp[0]; // 全局最大和
for (int i = 1; i < n; i++) {
// 状态转移:选择当前元素单独成组 或 加入前一个子数组
dp[i] = Math.max(nums[i], dp[i-1] + nums[i]);
// 更新全局最大值
maxSum = Math.max(maxSum, dp[i]);
}
return maxSum;
}
}
方法二:动态规划(空间优化)
class Solution {
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int currentSum = nums[0]; // 当前子数组和(空间优化版 dp[i])
int maxSum = nums[0]; // 全局最大和
for (int i = 1; i < nums.length; i++) {
// 状态转移:选择当前元素单独成组 或 加入前一个子数组
currentSum = Math.max(nums[i], currentSum + nums[i]);
// 更新全局最大值
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}
}
算法分析
- 时间复杂度:O(n)
只需遍历数组一次。 - 空间复杂度:
- 方法一:O(n),使用 DP 数组存储中间状态。
- 方法二:O(1),仅需两个变量。
算法过程
输入:nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
- 初始化:
currentSum = -2
,maxSum = -2
- i=1(元素 1):
currentSum = max(1, -2+1= -1) = 1
maxSum = max(-2, 1) = 1
- i=2(元素 -3):
currentSum = max(-3, 1-3= -2) = -2
maxSum = max(1, -2) = 1
- i=3(元素 4):
currentSum = max(4, -2+4= 2) = 4
maxSum = max(1, 4) = 4
- i=4(元素 -1):
currentSum = max(-1, 4-1= 3) = 3
maxSum = max(4, 3) = 4
- i=5(元素 2):
currentSum = max(2, 3+2= 5) = 5
maxSum = max(4, 5) = 5
- i=6(元素 1):
currentSum = max(1, 5+1= 6) = 6
maxSum = max(5, 6) = 6
- i=7(元素 -5):
currentSum = max(-5, 6-5= 1) = 1
maxSum = max(6, 1) = 6
- i=8(元素 4):
currentSum = max(4, 1+4= 5) = 5
maxSum = max(6, 5) = 6
最终结果:6
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1:标准示例
int[] nums1 = {-2,1,-3,4,-1,2,1,-5,4};
System.out.println("Test 1: " + solution.maxSubArray(nums1)); // 6
// 测试用例2:全正数数组
int[] nums2 = {1,2,3,4};
System.out.println("Test 2: " + solution.maxSubArray(nums2)); // 10
// 测试用例3:全负数数组
int[] nums3 = {-3,-1,-2,-5};
System.out.println("Test 3: " + solution.maxSubArray(nums3)); // -1
// 测试用例4:单元素数组
int[] nums4 = {5};
System.out.println("Test 4: " + solution.maxSubArray(nums4)); // 5
// 测试用例5:正负交替
int[] nums5 = {3,-2,5,-1};
System.out.println("Test 5: " + solution.maxSubArray(nums5)); // 6 (3-2+5=6)
// 测试用例6:空数组
int[] nums6 = {};
System.out.println("Test 6: " + solution.maxSubArray(nums6)); // 0
}
关键点
- 状态转移逻辑:
- 若前序子数组和为负数,则放弃前序部分(
currentSum = nums[i]
)。 - 若前序子数组和为正数,则加入前序部分(
currentSum += nums[i]
)。
- 若前序子数组和为负数,则放弃前序部分(
- 负数处理:
- 当全为负数时,算法会自动选择最大的单个负数(如测试用例3)。
- 空间优化:
- 仅需保存前一个状态,无需完整 DP 数组。
常见问题
- 为什么空间优化时不需要数组?
当前状态仅依赖前一个状态,用变量currentSum
代替数组即可。 - 如何处理全负数数组?
状态转移中的Math.max()
会自动选择最大单个值(如[-3,-1,-2]
中currentSum
会依次更新为-3 → -1 → -2
,最终maxSum=-1
)。 - 为何初始值设为
nums[0]
?
子数组至少包含一个元素,因此初始状态就是第一个元素本身。
LeetCode 152. 乘积最大子数组
问题描述
给定一个整数数组 nums
,找出乘积最大的非空连续子数组,并返回该子数组的乘积。
示例:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
算法思路
动态规划(DP):
- 问题:数组中存在负数时,负负得正可能导致最小值变最大值。
- 关键思路:同时维护两个 DP 数组:
maxDp[i]
:以nums[i]
结尾的子数组的最大乘积。minDp[i]
:以nums[i]
结尾的子数组的最小乘积。
- 状态转移:
- 当前值可能是正数或负数,需考虑三种情况:
- 当前值本身(
nums[i]
)。 - 当前值 × 前一个位置的最大乘积(
maxDp[i-1] * nums[i]
)。 - 当前值 × 前一个位置的最小乘积(
minDp[i-1] * nums[i]
)。
- 当前值本身(
- 转移方程:
maxDp[i] = max(nums[i], maxDp[i-1] * nums[i], minDp[i-1] * nums[i]) minDp[i] = min(nums[i], maxDp[i-1] * nums[i], minDp[i-1] * nums[i])
- 当前值可能是正数或负数,需考虑三种情况:
- 结果更新:遍历过程中记录
maxDp
中的最大值。
代码实现
方法一:动态规划(DP 数组)
class Solution {
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
// 创建两个 DP 数组:
// maxDp[i] 表示以 nums[i] 结尾的子数组的最大乘积
// minDp[i] 表示以 nums[i] 结尾的子数组的最小乘积
int[] maxDp = new int[n];
int[] minDp = new int[n];
// 初始化:第一个元素的最大和最小乘积都是它本身
maxDp[0] = nums[0];
minDp[0] = nums[0];
int maxProduct = nums[0]; // 全局最大乘积
for (int i = 1; i < n; i++) {
// 计算三种候选值:
// 1. 当前数字本身
// 2. 当前数字乘以前一个位置的最大乘积
// 3. 当前数字乘以前一个位置的最小乘积
int candidate1 = nums[i]; // 当前元素自身
int candidate2 = maxDp[i - 1] * nums[i]; // 乘以前一个最大乘积
int candidate3 = minDp[i - 1] * nums[i]; // 乘以前一个最小乘积
// 更新当前最大乘积:取三种候选值的最大值
maxDp[i] = Math.max(candidate1, Math.max(candidate2, candidate3));
// 更新当前最小乘积:取三种候选值的最小值
minDp[i] = Math.min(candidate1, Math.min(candidate2, candidate3));
// 更新全局最大乘积
if (maxDp[i] > maxProduct) {
maxProduct = maxDp[i];
}
}
return maxProduct;
}
}
方法二:动态规划(空间优化)
class Solution {
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// 使用变量代替 DP 数组:
int maxSoFar = nums[0]; // 当前最大乘积
int minSoFar = nums[0]; // 当前最小乘积
int maxProduct = nums[0]; // 全局最大乘积
for (int i = 1; i < nums.length; i++) {
// 保存旧值(避免覆盖)
int prevMax = maxSoFar;
int prevMin = minSoFar;
// 更新当前最大乘积(三种候选值)
maxSoFar = Math.max(nums[i], Math.max(prevMax * nums[i], prevMin * nums[i]));
// 更新当前最小乘积(三种候选值)
minSoFar = Math.min(nums[i], Math.min(prevMax * nums[i], prevMin * nums[i]));
// 更新全局最大乘积
if (maxSoFar > maxProduct) {
maxProduct = maxSoFar;
}
}
return maxProduct;
}
}
算法分析
- 时间复杂度:O(n),遍历数组一次。
- 空间复杂度:
- 方法一:O(n),使用两个 DP 数组。
- 方法二:O(1),仅使用常数空间。
算法过程
输入:nums = [2, 3, -2, 4]
- 初始化:
maxDp[0] = minDp[0] = 2
,maxProduct = 2
- i=1(元素 3):
candidate1 = 3
candidate2 = 2*3 = 6
candidate3 = 2*3 = 6
maxDp[1] = max(3,6,6)=6
minDp[1] = min(3,6,6)=3
maxProduct = max(2,6)=6
- i=2(元素 -2):
candidate1 = -2
candidate2 = 6*(-2) = -12
candidate3 = 3*(-2) = -6
maxDp[2] = max(-2, -12, -6) = -2
minDp[2] = min(-2, -12, -6) = -12
maxProduct = max(6, -2) = 6
- i=3(元素 4):
candidate1 = 4
candidate2 = -2*4 = -8
candidate3 = -12*4 = -48
maxDp[3] = max(4, -8, -48) = 4
minDp[3] = min(4, -8, -48) = -48
maxProduct = max(6, 4) = 6
最终结果:6
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例 1: 标准示例
int[] nums1 = {2, 3, -2, 4};
System.out.println("Test 1: " + solution.maxProduct(nums1)); // 6
// 测试用例 2: 包含负数
int[] nums2 = {-2, 0, -1};
System.out.println("Test 2: " + solution.maxProduct(nums2)); // 0
// 测试用例 3: 全负数数组
int[] nums3 = {-2, -3, -1, -5};
System.out.println("Test 3: " + solution.maxProduct(nums3)); // 30
// 测试用例 4: 单个元素
int[] nums4 = {5};
System.out.println("Test 4: " + solution.maxProduct(nums4)); // 5
// 测试用例 5: 多个负数和正数
int[] nums5 = {2, -5, 3, 1, -2, 4};
System.out.println("Test 5: " + solution.maxProduct(nums5)); // 240(整个数组乘积)
// 测试用例 6: 空数组
int[] nums6 = {};
System.out.println("Test 6: " + solution.maxProduct(nums6)); // 0
// 测试用例 7: 两个负数
int[] nums7 = {-2, -3};
System.out.println("Test 7: " + solution.maxProduct(nums7)); // 6
}
关键点
- 负数处理:负负得正,需同时维护最大/最小值。
- 状态转移:
- 最大乘积 =
max(当前值, 前一个最大值×当前值, 前一个最小值×当前值)
- 最小乘积 =
min(当前值, 前一个最大值×当前值, 前一个最小值×当前值)
- 最大乘积 =
- 空间优化:只需前一个状态,用变量代替数组。
- 边界条件:空数组返回 0,单个元素返回自身。
常见问题
- 为什么需要同时维护最大值和最小值?
负数可能将最小值变为最大值(如[-2, -3]
中min=-2
,遇到-3
时-2*-3=6
成为最大值)。 - 空间优化时为何要保存旧值?
maxSoFar
和minSoFar
的更新依赖前一个状态,需用临时变量保存避免覆盖。 - 遇到 0 时如何处理?
当nums[i]=0
时:maxSoFar = max(0, 0, 0) = 0
minSoFar = min(0, 0, 0) = 0
后续计算会从 0 重新开始。