本文概览:本文以LeetCode经典题目"除自身以外数组的乘积"为例,从暴力解法入手,分析不能使用除法导致的重复计算问题,再通过空间换时间的方式用右累计乘积数组优化,将时间复杂度从 O(n²) 优化到 O(n)
一、题目

二、题目分析
给定一个整数数组 nums,返回数组 res,其中 res[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积
目标:计算每个位置除自身以外所有元素的乘积,且不能使用除法
核心问题:如果不限制除法,只需要算出总乘积再除以当前元素即可。但题目要求不能使用除法,这就迫使我们换一种方式来计算
思路概览
Java实现代码如下
Java
public int[] productExceptSelf(int[] nums) {
// 1.创建一个结果数组
int[] res = new int[nums.length];
// 2.创建一个右边的累计乘积数组
int[] right = new int[nums.length];
int rightMul = 1;
// 初始化右边的累计乘积数组的最后一个元素为1
right[nums.length - 1] = rightMul;
// 从右往左遍历,计算右边的累计乘积
for (int i = nums.length - 2; i >= 0; i--) {
rightMul *= nums[i + 1];
right[i] = rightMul;
}
// 3.创建一个左边的累计乘积变量
int leftMul = 1;
// 4.从左往右遍历,计算结果数组
for (int i = 0; i < nums.length; i++) {
res[i] = leftMul * right[i];
leftMul *= nums[i];
}
return res;
}
思路简要说明
-
右累计乘积数组 :
right[i]记录位置i右侧所有元素的乘积,从右往左一次遍历预计算 -
左累计乘积变量 :
leftMul记录位置i左侧所有元素的乘积,从左往右遍历时累乘即可,不需要额外数组 -
结果计算 :
res[i] = leftMul * right[i],左侧乘积和右侧乘积相乘就是除自身以外的乘积
三、思路详解
暴力解法入手
最自然的想法是:如果可以使用除法,只需要算出所有元素的总乘积,然后对每个位置除以 nums[i] 就能得到结果,时间复杂度 O(n)
但题目要求不能使用除法,那最直接的做法就是:每遍历到一个位置,分别累乘它左边的所有元素和右边的所有元素,然后相乘
如果可以使用除法,右边的累计乘积只需要算一次,之后每次除以当前遍历到的值就行了。但不能使用除法,就导致每次都要重新累乘右边的所有元素,而右边每次其实只变化了一个乘数,却要重复计算整个右边的乘积
- 时间复杂度:O(n²),对每个位置都要累乘左右两侧
- 核心瓶颈:右边的累计乘积每次只变化了一个乘数,却要重复计算整个乘积,做了大量无效计算
- 关键思考:能否把右边的累计乘积也像左边一样,用某种方式存储下来,避免重复计算?
左右累计乘积解法
思路分析
暴力解法的问题在于:左边的乘积可以从左往右累乘,每次只多乘一个数,不需要重复计算;但右边的乘积每次都要重新算,因为不能用除法把上一次的结果"除掉"一个数
那最简单而且自然的思路就是空间换时间 :创建一个数组 right[],用来记录每个位置对应的右边的累计乘积。只要从右往左遍历一次,就能得出这个数组。然后再对原数组从左往右遍历一次,左边的乘积用变量累乘,右边的乘积直接从 right[] 数组中 O(1) 读取
具体步骤
-
预计算右累计乘积数组 :从右往左遍历,
right[i] = nums[i+1] × nums[i+2] × ... × nums[n-1],即位置i右侧所有元素的乘积。用累乘的方式,rightMul *= nums[i+1],每次只多乘一个数 -
计算结果数组 :从左往右遍历,
leftMul累乘左侧元素,res[i] = leftMul * right[i],然后leftMul *= nums[i]
为什么左边用变量、右边用数组?
因为我们的遍历方向是从左往右,左边的乘积可以随着遍历逐步累乘,不需要存储历史值;而右边的乘积需要提前算好,因为从左往右遍历时,右边的乘积是"未来"的值,还没算到,所以必须预先存储
举例说明
以 nums = [1, 2, 3, 4] 为例
第一步:计算 right 数组
从右往左遍历:
| i | numsi+1 | rightMul | righti |
|---|---|---|---|
| 3 | - | 1 | right3 = 1 |
| 2 | 4 | 1×4=4 | right2 = 4 |
| 1 | 3 | 4×3=12 | right1 = 12 |
| 0 | 2 | 12×2=24 | right0 = 24 |
right = [24, 12, 4, 1]
第二步:计算结果数组
从左往右遍历:
| i | leftMul | righti | resi = leftMul × righti | leftMul 更新 |
|---|---|---|---|---|
| 0 | 1 | 24 | 1×24=24 | 1×1=1 |
| 1 | 1 | 12 | 1×12=12 | 1×2=2 |
| 2 | 2 | 4 | 2×4=8 | 2×3=6 |
| 3 | 6 | 1 | 6×1=6 | 6×4=24 |
最终结果为 [24, 12, 8, 6]
验证:2×3×4=24, 1×3×4=12, 1×2×4=8, 1×2×3=6,正确
- 时间复杂度:O(n),两次遍历
- 空间复杂度 :O(n),
right数组占用额外空间(结果数组res不算额外空间)