给你一个整数数组 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 * 104-10 <= nums[i] <= 10nums的任何子数组的乘积都 保证 是一个 32-位 整数
🏠 生活案例:人生的翻盘时刻
想象你在玩一个得分游戏,数组里的数字就是你每回合获得的“倍率”:
- 正数 (如 2) :让你的得分翻倍,大家都很喜欢。
- 零 (0) :最可怕,会让你的所有努力瞬间归零。
- 负数 (如 -2) :会让你的得分变成负数。
但是! 如果你现在手里已经有一个很大的负分,这时候再遇到一个负数,你就瞬间“翻身农奴把歌唱”,变成了一个巨大的正分。
所以,玩这个游戏你不能只盯着当前最高分,你还得偷偷记着当前的最低分(最有潜力的负数) ,万一遇到下一个负数,它就是你翻盘的希望。
💻 代码实现与生活化注释
这段代码在每一轮都会同时记录“最好”和“最坏”的情况。
JavaScript
/**
* @param {number[]} nums
* @return {number}
*/
var maxProduct = function(nums) {
let n = nums.length;
if (n === 0) return 0;
// 1. 初始化:minSub 记下当前最小值,maxSub 记下当前最大值
// result 是我们一路上见过的“最高光时刻”
let minSub = nums[0];
let maxSub = nums[0];
let result = nums[0];
// 2. 从第二个数字开始遍历
for (let i = 1; i < n; i++) {
let cur = nums[i];
// 3. 关键动作:因为 maxSub 会被更新,所以先用 temp 把“潜在的最大值”算出来
// 现在的最大值可能来自:
// - cur 自己(之前的分太烂了,从头开始)
// - minSub * cur(负负得正,翻盘了!)
// - maxSub * cur(正正得正,更上一层楼)
let temp = Math.max(cur, minSub * cur, maxSub * cur);
// 4. 更新最小规模(为了下次遇到负数时能翻盘)
minSub = Math.min(cur, minSub * cur, maxSub * cur);
// 5. 更新最大规模
maxSub = temp;
// 6. 看看这一轮出来的最大值,能不能打破历史纪录
result = Math.max(result, maxSub);
}
return result;
};
🧩 核心逻辑:为什么要存最小值?
看这个例子:[2, 3, -2, -4]
-
走完
2, 3:最大值是6,最小值是2。 -
遇到
-2:- 最大值变成
6 * -2 = -12?不,它会选自己-2。 - 最小值变成了
6 * -2 = -12。虽然现在很难看,但它存下了“最大的负债”。
- 最大值变成
-
遇到
-4:- 最大值对比:
-4vs-12 * -4 (48)vs-2 * -4 (8)。 - Boom!
48出现了。正是因为我们记住了那一刻的“最坏情况”-12,才有了这一刻的超级翻盘。
- 最大值对比:
💡 算法复杂度
- 时间复杂度:。我们只排队走了一次,没回头。
- 空间复杂度:。我们只用了三个变量
minSub,maxSub,result在记账,非常省内存。
总结一下:
这题的精髓就在于:永远不要小看低谷(最小值),因为它配合下一个负号,就是你巅峰(最大值)的起点。