“前缀和”大家都会,但“前缀积”呢?
这道题不考算法套路,只考你对前后缀思想的理解深度。
看完本文,你不仅能秒杀本题,更能举一反三解决所有“排除自身”的数组问题!
一、题目回顾:除自身以外数组的乘积
给定一个整数数组 nums,返回数组 answer,其中:
answer[i] = nums 中除 nums[i] 外所有元素的乘积- 不能使用除法
- 时间复杂度 O(n)
- 进阶:空间复杂度 O(1) (输出数组不算额外空间)
- 可恶,竟然把我的暴力法宝禁用了
- 糟糕,连除法小捷径也给我堵了。
- 连递归也想到了码,想的还真是全面啊。
示例
输入: [1,2,3,4]
输出: [24,12,8,6] // 2*3*4=24, 1*3*4=12, ...
输入: [-1,1,0,-3,3]
输出: [0,0,9,0,0] // 只有 index=2 时,其他元素乘积为 (-1)*1*(-3)*3 = 9
二、暴力解法(O(n²))—— 为什么不行?
js
编辑
// ❌ 暴力法:对每个 i,遍历其他所有元素
var productExceptSelf = function(nums) {
const n = nums.length;
const answer = [];
for (let i = 0; i < n; i++) {
let product = 1;
for (let j = 0; j < n; j++) {
if (j !== i) product *= nums[j];
}
answer.push(product);
}
return answer;
};
问题:时间复杂度 O(n²),对于 n = 10⁵ 会超时!
关键洞察:我们重复计算了大量相同的乘积!
- 你以为这就结束了,NO,孙悟空都有72变,我还不能有第3件法宝了。
三、核心思想:前缀积 × 后缀积 = 答案!
(既然不能用除法,还有时间复杂度要求,我不盐了,法宝前后缀思想来也)
🧠 灵魂一问:
对于位置
i,answer[i]等于什么?
答:左边所有数的乘积 × 右边所有数的乘积
text
编辑
nums = [a, b, c, d, e]
answer[2] = (a * b) * (d * e)
= prefix[2] * suffix[2]
✅ 定义:
- 前缀积
prefix[i]=nums[0] × nums[1] × ... × nums[i-1] - 后缀积
suffix[i]=nums[i+1] × ... × nums[n-1] - answer[i] = prefix[i] × suffix[i]
💡 注意:
prefix[0] = 1(左边无元素),suffix[n-1] = 1(右边无元素)
四、方法一:双数组法(O(n) 空间)—— 理解思想
js
编辑
var productExceptSelf = function(nums) {
const n = nums.length;
// 1. 计算前缀积
const prefix = new Array(n).fill(1);
for (let i = 1; i < n; i++) {
prefix[i] = prefix[i - 1] * nums[i - 1];
}
// 2. 计算后缀积
const suffix = new Array(n).fill(1);
for (let i = n - 2; i >= 0; i--) {
suffix[i] = suffix[i + 1] * nums[i + 1];
}
// 3. 合并结果
const answer = [];
for (let i = 0; i < n; i++) {
answer[i] = prefix[i] * suffix[i];
}
return answer;
};
📊 过程演示(nums = [1,2,3,4])
| i | nums[i] | prefix[i] | suffix[i] | answer[i] |
|---|---|---|---|---|
| 0 | 1 | 1 | 2×3×4=24 | 24 |
| 1 | 2 | 1 | 3×4=12 | 12 |
| 2 | 3 | 1×2=2 | 4 | 8 |
| 3 | 4 | 1×2×3=6 | 1 | 6 |
✅ 完美匹配!
五、方法二:O(1) 空间优化 —— 秒杀进阶要求!
关键洞察:我们不需要同时存储整个
prefix和suffix数组!
可以复用输出数组answer,先存前缀积,再从右往左乘以后缀积。
🔥 终极实现
js
编辑
var productExceptSelf = function(nums) {
const n = nums.length;
const answer = new Array(n);
// 第一步:answer 作为前缀积数组
answer[0] = 1;
for (let i = 1; i < n; i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}
// 第二步:从右往左,用一个变量记录后缀积
let suffix = 1;
for (let i = n - 1; i >= 0; i--) {
answer[i] = answer[i] * suffix; // 前缀 × 后缀
suffix = suffix * nums[i]; // 更新后缀积
}
return answer;
};
🔄 执行过程(nums = [1,2,3,4])
第一遍(计算前缀积到 answer) :
text
编辑
answer = [1, 1, 2, 6]
第二遍(从右往左乘以后缀积) :
| i | suffix (进入循环前) | answer[i] = answer[i] * suffix | 更新 suffix = suffix * nums[i] |
|---|---|---|---|
| 3 | 1 | 6 * 1 = 6 | 1 * 4 = 4 |
| 2 | 4 | 2 * 4 = 8 | 4 * 3 = 12 |
| 1 | 12 | 1 * 12 = 12 | 12 * 2 = 24 |
| 0 | 24 | 1 * 24 = 24 | 24 * 1 = 24 |
最终 answer = [24, 12, 8, 6] ✅
六、为什么这个解法是 O(1) 空间?
- 输入数组
nums:不算额外空间(题目给定) - 输出数组
answer:题目明确说明“不被视为额外空间” - 唯一额外变量:
suffix(一个整数)
📌 空间复杂度 = O(1) ,完美满足进阶要求!
七、边界情况处理
情况 1:包含 0
js
编辑
nums = [-1, 1, 0, -3, 3]
- 当
i = 2(值为 0)时,answer[2] = (-1)*1*(-3)*3 = 9 - 其他位置都包含 0,所以结果为 0
✅ 算法天然处理 0,无需特殊逻辑!
情况 2:负数
js
编辑
nums = [-1, -2, -3]
answer = [6, 3, 2] // (-2)*(-3)=6, (-1)*(-3)=3, (-1)*(-2)=2
✅ 正负号自动处理!
八、前后缀思想的通用模板
这道题的本质是 “排除当前元素的区间聚合” 。你可以套用以下模板:
// 通用前后缀解法框架
function solve(nums) {
const n = nums.length;
const result = new Array(n);
// 1. 从左到右:计算前缀
result[0] = base; // 通常是 1(乘积)或 0(求和)
for (let i = 1; i < n; i++) {
result[i] = result[i-1] OP nums[i-1]; // OP 是 * 或 +
}
// 2. 从右到左:合并后缀
let suffix = base;
for (let i = n-1; i >= 0; i--) {
result[i] = result[i] OP suffix;
suffix = suffix OP nums[i];
}
return result;
}
💡 把
OP换成+,就能解决“除自身以外的和”问题!
九、总结:前后缀思想的威力
| 要点 | 说明 |
|---|---|
| 核心思想 | 将“全局计算”拆分为“左半部分 + 右半部分” |
| 时间复杂度 | O(n) —— 只需两次遍历 |
| 空间优化 | 复用输出数组,额外空间 O(1) |
| 适用场景 | 所有“排除当前元素”的聚合问题(乘积、求和、最大值等) |
| 面试价值 | 高频题!考察对基础思想的理解深度 |
💬 最后忠告:
不要死记代码!理解 “前缀 + 后缀 = 答案” 这一思想,
你就能在 30 秒内手撕所有类似题目!
现在,打开你的编辑器,亲手敲一遍代码。下次遇到“除自身以外...”的问题,你就是全场最快的男人,当然不能只会做这道题目,遇到类似的也要举一反三!💪