你真的懂前缀积吗?一道题检验你是否掌握前后缀思想,以后遇到就是秒杀!不会再束手无策!

74 阅读5分钟

“前缀和”大家都会,但“前缀积”呢?
这道题不考算法套路,只考你对前后缀思想的理解深度。
看完本文,你不仅能秒杀本题,更能举一反三解决所有“排除自身”的数组问题!


一、题目回顾:除自身以外数组的乘积

给定一个整数数组 nums,返回数组 answer,其中:

  • answer[i] = nums 中除 nums[i] 外所有元素的乘积
  • 不能使用除法
  • 时间复杂度 O(n)
  • 进阶:空间复杂度 O(1) (输出数组不算额外空间)
  • 可恶,竟然把我的暴力法宝禁用了
  • 糟糕,连除法小捷径也给我堵了。
  • 连递归也想到了码,想的还真是全面啊。

image.png

示例


输入: [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件法宝了。
  • image.png

三、核心思想:前缀积 × 后缀积 = 答案!

(既然不能用除法,还有时间复杂度要求,我不盐了,法宝前后缀思想来也) image.png

🧠 灵魂一问:

对于位置 ianswer[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])

inums[i]prefix[i]suffix[i]answer[i]
0112×3×4=2424
1213×4=1212
231×2=248
341×2×3=616

完美匹配!


五、方法二:O(1) 空间优化 —— 秒杀进阶要求!

关键洞察:我们不需要同时存储整个 prefixsuffix 数组!
可以复用输出数组 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]

第二遍(从右往左乘以后缀积)

isuffix (进入循环前)answer[i] = answer[i] * suffix更新 suffix = suffix * nums[i]
316 * 1 = 61 * 4 = 4
242 * 4 = 84 * 3 = 12
1121 * 12 = 1212 * 2 = 24
0241 * 24 = 2424 * 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 秒内手撕所有类似题目!

image.png 现在,打开你的编辑器,亲手敲一遍代码。下次遇到“除自身以外...”的问题,你就是全场最快的男人,当然不能只会做这道题目,遇到类似的也要举一反三!💪