LeetCode Hot100(71/100)——152. 乘积最大子数组

文章目录

题目链接

LeetCode 152. 乘积最大子数组

题目说明

给你一个整数数组 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 位整数

问题分析

这道题的核心难点在于处理负数。与求和不同,乘积运算有以下特点:

  1. 负数乘以负数会变成正数
  2. 遇到 0 会使乘积归零
  3. 当前的最小值(负数)可能在下一步变成最大值

正数
负数



遍历数组
当前元素
最大值变更大

最小值变更小
最大值和最小值互换
重置为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,那么最大乘积子数组要么是整个数组,要么是去掉前面若干元素,要么是去掉后面若干元素。

核心思想:

  1. 从左到右遍历一次,计算累积乘积
  2. 从右到左遍历一次,计算累积乘积
  3. 遇到 0 时重置乘积为 1
  4. 记录过程中的最大值

右向左遍历 数组 左向右遍历 右向左遍历 数组 左向右遍历 遇到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) 较难 中等 ⭐⭐⭐

推荐使用解法一(动态规划-双状态维护),因为它在时间和空间上都达到了最优,代码简洁且易于理解。解法二适合初学者理解动态规划的思想,解法三则是一个巧妙的技巧,适合面试时展示思维能力。

相关推荐
Z9fish1 小时前
sse哈工大C语言编程练习44
c语言·c++·算法
李日灐1 小时前
改造红黑树实现封装 map/set:感受C++ 标准容器的精妙设计与底层实现
开发语言·数据结构·c++·后端·算法·红黑树
李日灐2 小时前
【优选算法1】双指针经典算法题
数据结构·c++·后端·算法·刷题·双指针
Frostnova丶2 小时前
(9)LeetCode 438.找到字符串中所有字母异位词
算法·leetcode
故事和你912 小时前
sdut-程序设计基础Ⅰ-期末测试(重现)
大数据·开发语言·数据结构·c++·算法·蓝桥杯·图论
telephonegram2 小时前
只会写代码迟早被淘汰?大专IT生向“业务架构师”转型的黄金路径
面试·职场和发展·学习方法
努力学算法的蒟蒻2 小时前
day114(3.16)——leetcode面试经典150
算法·leetcode·职场和发展
ysa0510302 小时前
贪心【逆向dp】
数据结构·c++·笔记·算法
夜月yeyue2 小时前
Linux 邻接(Neighbor)子系统架构与 NUD 状态机
linux·运维·服务器·嵌入式硬件·算法·系统架构