对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎
LeetCode 152. 乘积最大子数组
1. 题目描述
1.1 问题定义
给你一个整数数组 nums,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
1.2 关键约束
1 <= nums.length <= 2 * 10^4-10 <= nums[i] <= 10- 测试用例的答案是一个32-位整数
- 数组元素可能包含负数、零、正数
1.3 示例说明
javascript
// 示例 1
输入: [2, 3, -2, 4]
输出: 6
解释: 子数组 [2, 3] 有最大乘积 6
// 示例 2
输入: [-2, 0, -1]
输出: 0
解释: 结果不能为 2, 因为 [-2, -1] 不是子数组
2. 问题分析
2.1 核心难点
与"最大子数组和"(LeetCode 53)不同,乘积的最大子数组问题存在独特的挑战:
- 负数效应:负数乘以负数得到正数,意味着当前的最小负数乘积可能在未来遇到另一个负数时变为最大正数
- 零值重置:遇到零时,乘积会重置为零,之前的乘积积累完全失效
- 符号变化:乘积的符号会随着负数的个数而变化
2.2 前端开发场景类比
前端开发中也会遇到类似场景:
- 计算连续时间段的用户增长乘积
- 处理股票价格连续涨跌的累计收益
- 计算动画效果连续变化的累积影响
3. 解题思路
3.1 思路一:暴力枚举(不可行)
javascript
// 时间复杂度:O(n²) - 对于 n=20000 会超时
// 空间复杂度:O(1)
// 评估:理论可行,实际超时
3.2 思路二:动态规划(最优解)
javascript
// 时间复杂度:O(n)
// 空间复杂度:O(1)
// 评估:最优解,一次遍历解决问题
核心思想 :同时维护到当前位置的最大乘积 和最小乘积,因为最小乘积(负数)可能在遇到另一个负数时变为最大乘积。
4. 代码实现
4.1 暴力解法(理解问题)
javascript
/**
* 暴力解法 - 仅用于理解问题,实际会超时
* @param {number[]} nums
* @return {number}
*/
var maxProductBruteForce = function(nums) {
if (nums.length === 0) return 0;
let maxProduct = -Infinity;
// 枚举所有可能的子数组起点
for (let i = 0; i < nums.length; i++) {
let currentProduct = 1;
// 枚举所有可能的子数组终点
for (let j = i; j < nums.length; j++) {
currentProduct *= nums[j];
maxProduct = Math.max(maxProduct, currentProduct);
}
}
return maxProduct;
};
// 测试用例
console.log(maxProductBruteForce([2, 3, -2, 4])); // 6
console.log(maxProductBruteForce([-2, 0, -1])); // 0
4.2 动态规划解法(标准实现)
javascript
/**
* 动态规划解法 - 最优解
* @param {number[]} nums
* @return {number}
*/
var maxProduct = function(nums) {
if (nums.length === 0) return 0;
// 初始化:第一个元素的最大和最小乘积都是它本身
let maxSoFar = nums[0]; // 到当前位置的最大乘积
let minSoFar = nums[0]; // 到当前位置的最小乘积
let result = nums[0]; // 全局最大乘积
// 从第二个元素开始遍历
for (let i = 1; i < nums.length; i++) {
const current = nums[i];
// 关键步骤:同时计算三个可能性
// 1. 当前元素本身
// 2. 当前元素 × 之前的最大乘积
// 3. 当前元素 × 之前的最小乘积(负数可能变正数)
const tempMax = Math.max(
current, // 情况1:重新开始
current * maxSoFar, // 情况2:延续最大
current * minSoFar // 情况3:最小变最大
);
const tempMin = Math.min(
current, // 情况1:重新开始
current * maxSoFar, // 情况2:延续最小
current * minSoFar // 情况3:最大变最小
);
// 更新状态
maxSoFar = tempMax;
minSoFar = tempMin;
// 更新全局结果
result = Math.max(result, maxSoFar);
}
return result;
};
4.3 动态规划解法(优化版)
javascript
/**
* 动态规划解法 - 优化版(使用数组缓存)
* @param {number[]} nums
* @return {number}
*/
var maxProductOptimized = function(nums) {
const n = nums.length;
if (n === 0) return 0;
// dpMax[i]: 以 nums[i] 结尾的子数组的最大乘积
// dpMin[i]: 以 nums[i] 结尾的子数组的最小乘积
const dpMax = new Array(n);
const dpMin = new Array(n);
// 初始化
dpMax[0] = nums[0];
dpMin[0] = nums[0];
let result = nums[0];
for (let i = 1; i < n; i++) {
dpMax[i] = Math.max(
nums[i],
nums[i] * dpMax[i - 1],
nums[i] * dpMin[i - 1]
);
dpMin[i] = Math.min(
nums[i],
nums[i] * dpMax[i - 1],
nums[i] * dpMin[i - 1]
);
result = Math.max(result, dpMax[i]);
}
return result;
};
4.4 动态规划解法(空间优化版)
javascript
/**
* 动态规划解法 - 空间优化版(O(1)空间复杂度)
* @param {number[]} nums
* @return {number}
*/
var maxProductSpaceOptimized = function(nums) {
if (nums.length === 0) return 0;
let maxProduct = nums[0];
let minProduct = nums[0];
let result = nums[0];
for (let i = 1; i < nums.length; i++) {
// 如果当前数是负数,交换最大和最小
// 因为负数会让大的变小,小的变大
if (nums[i] < 0) {
[maxProduct, minProduct] = [minProduct, maxProduct];
}
// 更新最大和最小乘积
maxProduct = Math.max(nums[i], maxProduct * nums[i]);
minProduct = Math.min(nums[i], minProduct * nums[i]);
// 更新全局结果
result = Math.max(result, maxProduct);
}
return result;
};
5. 各实现思路的复杂度、优缺点对比
5.1 复杂度对比表格
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 暴力枚举 | O(n²) | O(1) | 思路简单,易于实现 | 效率极低,n=20000时超时 | 仅用于教学理解 |
| 标准动态规划 | O(n) | O(n) | 逻辑清晰,易于理解 | 空间使用可以优化 | 一般场景 |
| 空间优化动态规划 | O(n) | O(1) | 最优空间效率 | 状态转移需要仔细处理 | 大规模数据 |
| 交换法动态规划 | O(n) | O(1) | 代码简洁,利用负数特性 | 需要理解交换逻辑 | 推荐使用 |
5.2 步骤分解说明
以输入 [2, 3, -2, 4] 为例:
| 步骤 | i | nums[i] | maxSoFar | minSoFar | result |
|---|---|---|---|---|---|
| 初始化 | - | - | 2 | 2 | 2 |
| 第1步 | 1 | 3 | max(3, 2×3, 2×3)=6 | min(3, 2×3, 2×3)=3 | max(2,6)=6 |
| 第2步 | 2 | -2 | max(-2, 6×-2, 3×-2)=-2 | min(-2, 6×-2, 3×-2)=-12 | max(6,-2)=6 |
| 第3步 | 3 | 4 | max(4, -2×4, -12×4)=4 | min(4, -2×4, -12×4)=-48 | max(6,4)=6 |
最终结果:6
6. 总结
6.1 通用解题模板
javascript
/**
* 处理"乘积最值"类问题的通用模板
* @param {number[]} nums
* @return {number}
*/
function productMaxMinTemplate(nums) {
if (nums.length === 0) return 0;
// 1. 初始化状态变量
let maxProd = nums[0]; // 当前最大乘积
let minProd = nums[0]; // 当前最小乘积
let result = nums[0]; // 全局结果
// 2. 遍历数组
for (let i = 1; i < nums.length; i++) {
const current = nums[i];
// 3. 根据当前值特性调整状态(如有负数需交换)
if (current < 0) {
[maxProd, minProd] = [minProd, maxProd];
}
// 4. 更新状态:考虑重新开始或延续
maxProd = Math.max(current, maxProd * current);
minProd = Math.min(current, minProd * current);
// 5. 更新全局结果
result = Math.max(result, maxProd);
}
return result;
}
6.2 核心思想总结
- 双重状态维护:同时跟踪最大和最小乘积
- 负数处理:负数会反转大小关系,需要特殊处理
- 零值处理:遇到零时乘积重置
- 重新开始可能:当前元素本身可能比延续乘积更好
6.3 类似题目推荐
| 题目编号 | 题目名称 | 相似点 | 差异点 | 前端应用场景 |
|---|---|---|---|---|
| 53 | 最大子数组和 | 连续子数组问题 | 只有加法,无需考虑符号变化 | 连续时间段收益计算 |
| 628 | 三个数的最大乘积 | 乘积最值问题 | 非连续,固定数量 | 商品组合推荐 |
| 713 | 乘积小于K的子数组 | 乘积相关问题 | 需要统计个数,不是最大 | 连续事件阈值统计 |
| 238 | 除自身以外数组的乘积 | 乘积计算 | 需要排除自身元素 | 数据预处理计算 |