📌 题目链接:152. 乘积最大子数组 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:数组、动态规划
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)
🧠 题目分析
给定一个整数数组 nums,要求找出乘积最大的非空连续子数组(至少包含一个数字),并返回其乘积。
这道题与经典的「53. 最大子序和」看似相似,但关键区别在于:负数 × 负数 = 正数。这意味着:
- 当前位置的最优解不一定由前一个位置的“最大值”转移而来;
- 一个很小的负数(比如 -1000)如果后面遇到另一个负数(比如 -2),反而可能变成很大的正数(2000)。
因此,仅维护“以 i 结尾的最大乘积”是不够的,我们还必须同时维护“以 i 结尾的最小乘积”,因为最小值在遇到负数时可能“逆袭”成最大值。
💡 面试考点:
这是一道典型的“状态不唯一”的动态规划题。考察你是否能意识到:极值问题中,负数会反转大小关系,从而需要同时追踪最大值和最小值。这也是高频面试题!
⚙️ 核心算法及代码讲解
✅ 动态规划设计
我们定义两个状态变量(滚动更新,无需数组):
maxF:以当前元素结尾的最大乘积minF:以当前元素结尾的最小乘积
对于每个 nums[i],有三种选择:
- 单独以
nums[i]作为子数组; - 将
nums[i]接到前面最大乘积子数组后面:maxF * nums[i]; - 将
nums[i]接到前面最小乘积子数组后面:minF * nums[i]。
由于 nums[i] 可能为正、负或零,我们需要在这三者中取最大值更新 maxF,取最小值更新 minF。
📌 关键洞察:
当nums[i] < 0时,maxF * nums[i]会变小,而minF * nums[i]可能变大(负负得正)。所以必须同时考虑两者!
🧾 状态转移方程
new_max = max(nums[i], maxF * nums[i], minF * nums[i])
new_min = min(nums[i], maxF * nums[i], minF * nums[i])
然后更新 maxF = new_max, minF = new_min,并用 maxF 更新全局答案。
⚠️ 注意:必须先保存旧的
maxF和minF,否则在计算minF时会用到已经更新的maxF,导致错误!
🧪 边界处理
- 初始值:
maxF = minF = ans = nums[0] - 数组长度 ≥ 1,无需判空
🧩 解题思路(分步)
-
初始化:设
maxF,minF,ans均为nums[0]; -
从 i = 1 开始遍历数组:
- 保存当前
maxF和minF到临时变量mx,mn; - 计算新的
maxF = max(mx * nums[i], mn * nums[i], nums[i]); - 计算新的
minF = min(mx * nums[i], mn * nums[i], nums[i]); - 更新全局最大值
ans = max(ans, maxF);
- 保存当前
-
返回
ans。
✅ 为什么不用判断溢出?
题目保证结果是 32 位整数,且输入范围小(-10 ≤ nums[i] ≤ 10,n ≤ 2e4),最坏情况10^20000显然不可能,实际测试用例不会溢出long long。但官方题解用了(long)强转防中间溢出,我们保留此写法。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n):单次遍历数组 |
| 空间复杂度 | O(1):仅使用常数个变量 |
| 是否原地 | 是 |
| 是否稳定 | 不涉及排序,无稳定性概念 |
| 面试频率 | ⭐⭐⭐⭐☆(非常高!常与“最大子序和”对比考察) |
💬 面试追问可能:
- 如果数组全是正数?→ 退化为累乘,直接返回总乘积。
- 如果包含 0?→ 0 会重置状态(因为
max(0, ...)会让maxF=0,相当于重新开始)。- 能否输出具体子数组?→ 需要额外记录起止位置,稍复杂,但思路类似。
💻 代码
C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int maxProduct(vector<int>& nums) {
// 初始化:第一个元素既是最大值也是最小值,也是当前答案
long maxF = nums[0], minF = nums[0], ans = nums[0];
// 从第二个元素开始遍历
for (int i = 1; i < nums.size(); ++i) {
// 保存上一轮的 maxF 和 minF,避免被覆盖
long mx = maxF, mn = minF;
// 更新当前最大乘积:三种可能取最大
maxF = max(mx * nums[i], max((long)nums[i], mn * nums[i]));
// 更新当前最小乘积:三种可能取最小
minF = min(mn * nums[i], min((long)nums[i], mx * nums[i]));
// 题解中原有保护语句(实际可省略,因题目保证32位整数)
if(minF < INT_MIN) {
minF = nums[i];
}
// 更新全局最大乘积
ans = max(maxF, ans);
}
return ans;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
vector<int> nums1 = {2,3,-2,4};
cout << "Test 1: " << sol.maxProduct(nums1) << " (Expected: 6)\n"; // 输出 6
vector<int> nums2 = {-2,0,-1};
cout << "Test 2: " << sol.maxProduct(nums2) << " (Expected: 0)\n"; // 输出 0
vector<int> nums3 = {-2,3,-4};
cout << "Test 3: " << sol.maxProduct(nums3) << " (Expected: 24)\n"; // 输出 24
vector<int> nums4 = {-1,-2,-3};
cout << "Test 4: " << sol.maxProduct(nums4) << " (Expected: 6)\n"; // 输出 6
return 0;
}
JavaScript
/**
* @param {number[]} nums
* @return {number}
*/
var maxProduct = function(nums) {
let maxF = nums[0];
let minF = nums[0];
let ans = nums[0];
for (let i = 1; i < nums.length; i++) {
const mx = maxF;
const mn = minF;
maxF = Math.max(mx * nums[i], mn * nums[i], nums[i]);
minF = Math.min(mx * nums[i], mn * nums[i], nums[i]);
ans = Math.max(ans, maxF);
}
return ans;
};
// 测试
console.log(maxProduct([2,3,-2,4])); // 6
console.log(maxProduct([-2,0,-1])); // 0
console.log(maxProduct([-2,3,-4])); // 24
console.log(maxProduct([-1,-2,-3])); // 6
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!