【中等】152. 乘积最大子数组

0 阅读3分钟

给你一个整数数组 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] <= 10
  • nums 的任何子数组的乘积都 保证 是一个 32-位 整数

🏠 生活案例:人生的翻盘时刻

想象你在玩一个得分游戏,数组里的数字就是你每回合获得的“倍率”:

  1. 正数 (如 2) :让你的得分翻倍,大家都很喜欢。
  2. 零 (0) :最可怕,会让你的所有努力瞬间归零。
  3. 负数 (如 -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

    • 最大值对比:-4 vs -12 * -4 (48) vs -2 * -4 (8)
    • Boom! 48 出现了。正是因为我们记住了那一刻的“最坏情况” -12,才有了这一刻的超级翻盘。

💡 算法复杂度

  • 时间复杂度O(n)O(n)。我们只排队走了一次,没回头。
  • 空间复杂度O(1)O(1)。我们只用了三个变量 minSub, maxSub, result 在记账,非常省内存。

总结一下:

这题的精髓就在于:永远不要小看低谷(最小值),因为它配合下一个负号,就是你巅峰(最大值)的起点。