文章目录
题目链接
题目说明
给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32 位整数。
示例 1:
输入: nums = [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: nums = [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
提示:
- 1 <= nums.length <= 2 × 10⁴
- -10 <= nums[i] <= 10
nums的任何前缀或后缀的乘积都保证是一个 32 位整数
问题分析
这道题的核心难点在于处理负数。与求和不同,乘积运算有以下特点:
- 负数乘以负数会变成正数
- 遇到 0 会使乘积归零
- 当前的最小值(负数)可能在下一步变成最大值
正数
负数
零
否
是
遍历数组
当前元素
最大值变更大
最小值变更小
最大值和最小值互换
重置为0
更新全局最大值
是否遍历完
返回结果
解法一:动态规划(双状态维护)
原理讲解
这是最优雅的解法。核心思想是同时维护两个状态:
maxProduct:以当前位置结尾的子数组的最大乘积minProduct:以当前位置结尾的子数组的最小乘积
为什么要维护最小值?因为当遇到负数时,之前的最小值(可能是很大的负数)乘以当前负数会变成很大的正数。
状态转移方程:
maxProduct = max(nums[i], maxProduct * nums[i], minProduct * nums[i])
minProduct = min(nums[i], maxProduct * nums[i], minProduct * nums[i])
当前元素
计算三个候选值
nums_i
maxProduct × nums_i
minProduct × nums_i
取最大值作为新maxProduct
取最小值作为新minProduct
Java实现
java
class Solution {
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
// 初始化最大值、最小值和结果
int maxProduct = nums[0];
int minProduct = nums[0];
int result = nums[0];
// 从第二个元素开始遍历
for (int i = 1; i < nums.length; i++) {
int current = nums[i];
// 保存当前的maxProduct,因为计算minProduct时需要用到
int tempMax = maxProduct;
// 更新最大乘积:当前元素、当前元素×最大乘积、当前元素×最小乘积
maxProduct = Math.max(current, Math.max(tempMax * current, minProduct * current));
// 更新最小乘积
minProduct = Math.min(current, Math.min(tempMax * current, minProduct * current));
// 更新全局最大值
result = Math.max(result, maxProduct);
}
return result;
}
}
复杂度分析
- 时间复杂度:O(n),只需遍历数组一次
- 空间复杂度:O(1),只使用了常数个变量
解法二:动态规划(数组存储)
原理讲解
这个解法与解法一思路相同,但使用数组来存储每个位置的最大值和最小值,更直观地展示动态规划的过程。
dpMax数组
存储以i结尾的最大乘积
dpMin数组
存储以i结尾的最小乘积
状态转移
更新全局最大值
Java实现
java
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length;
// dpMax[i] 表示以 nums[i] 结尾的子数组的最大乘积
int[] dpMax = new int[n];
// dpMin[i] 表示以 nums[i] 结尾的子数组的最小乘积
int[] dpMin = new int[n];
// 初始化
dpMax[0] = nums[0];
dpMin[0] = nums[0];
int result = nums[0];
// 状态转移
for (int i = 1; i < n; i++) {
dpMax[i] = Math.max(nums[i], Math.max(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i]));
dpMin[i] = Math.min(nums[i], Math.min(dpMax[i-1] * nums[i], dpMin[i-1] * nums[i]));
result = Math.max(result, dpMax[i]);
}
return result;
}
}
复杂度分析
- 时间复杂度:O(n),遍历数组一次
- 空间复杂度:O(n),需要两个长度为 n 的数组
解法三:双向遍历
原理讲解
这是一个巧妙的解法。观察发现,如果数组中没有 0,那么最大乘积子数组要么是整个数组,要么是去掉前面若干元素,要么是去掉后面若干元素。
核心思想:
- 从左到右遍历一次,计算累积乘积
- 从右到左遍历一次,计算累积乘积
- 遇到 0 时重置乘积为 1
- 记录过程中的最大值
右向左遍历 数组 左向右遍历 右向左遍历 数组 左向右遍历 遇到0重置为1 遇到0重置为1 返回两次遍历的最大值 计算前缀乘积 更新最大值 计算后缀乘积 更新最大值
Java实现
java
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length;
int result = Integer.MIN_VALUE;
// 从左到右遍历
int product = 1;
for (int i = 0; i < n; i++) {
product *= nums[i];
result = Math.max(result, product);
// 遇到0重置
if (product == 0) {
product = 1;
}
}
// 从右到左遍历
product = 1;
for (int i = n - 1; i >= 0; i--) {
product *= nums[i];
result = Math.max(result, product);
// 遇到0重置
if (product == 0) {
product = 1;
}
}
return result;
}
}
复杂度分析
- 时间复杂度:O(n),需要遍历数组两次
- 空间复杂度:O(1),只使用常数个变量
测试用例演示
让我们用示例 [2,3,-2,4] 来演示解法一的执行过程:
初始: max=2, min=2, result=2
i=1, num=3
max=max_6,6,6_=6
min=min_3,6,6_=3
result=6
i=2, num=-2
max=max_-2,-12,-6_=-2
min=min_-2,-12,-6_=-12
result=6
i=3, num=4
max=max_4,-8,-48_=4
min=min_4,-8,-48_=-48
result=6
返回 6
解法对比分析
| 解法 | 时间复杂度 | 空间复杂度 | 实现难度 | 代码可读性 | 推荐指数 |
|---|---|---|---|---|---|
| 动态规划(双状态维护) | O(n) | O(1) | 中等 | 高 | ⭐⭐⭐⭐⭐ |
| 动态规划(数组存储) | O(n) | O(n) | 简单 | 很高 | ⭐⭐⭐⭐ |
| 双向遍历 | O(n) | O(1) | 较难 | 中等 | ⭐⭐⭐ |
推荐使用解法一(动态规划-双状态维护),因为它在时间和空间上都达到了最优,代码简洁且易于理解。解法二适合初学者理解动态规划的思想,解法三则是一个巧妙的技巧,适合面试时展示思维能力。