力扣解题-238. 除了自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除了 nums[i] 之外其余各元素的乘积 。
题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n) 时间复杂度内完成此题。
示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
提示:
2 <= nums.length <= 105
-30 <= nums[i] <= 30
输入 保证 数组 answer[i] 在 32 位 整数范围内
进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)
Related Topics
数组、前缀和
第一次解答
解题思路
核心方法:左右前缀乘积数组法,通过两个辅助数组分别存储“前缀乘积”和“后缀乘积”,再通过两者的组合计算每个位置的结果,逻辑直观且满足O(n)时间复杂度要求,但额外使用了两个数组导致空间复杂度为O(n)。
核心逻辑拆解
“除自身外所有元素的乘积”可拆解为“当前元素左边所有元素的乘积 × 右边所有元素的乘积”:
- 前缀乘积数组left:
left[i]表示nums[0]到nums[i]的累积乘积(即i位置及左侧所有元素的乘积); - 后缀乘积数组right:
right[i]表示nums[i]到nums[length-1]的累积乘积(即i位置及右侧所有元素的乘积); - 结果计算:
- 当
i=0(第一个元素):无左侧元素,结果=右侧所有元素的乘积=right[1]; - 当
i=length-1(最后一个元素):无右侧元素,结果=左侧所有元素的乘积=left[length-2]; - 其他位置:结果=左侧所有元素乘积(
left[i-1]) × 右侧所有元素乘积(right[i+1])。
- 当
具体步骤(以nums=[1,2,3,4]为例)
- 计算left数组:left[0]=1,left[1]=1×2=2,left[2]=2×3=6,left[3]=6×4=24;
- 计算right数组:right[3]=4,right[2]=3×4=12,right[1]=2×12=24,right[0]=1×24=24;
- 计算结果:
- result[0] = right[1] = 24;
- result[1] = left[0] × right[2] = 1×12=12;
- result[2] = left[1] × right[3] = 2×4=8;
- result[3] = left[2] = 6;
- 最终结果:[24,12,8,6]。
性能说明
- 时间复杂度:O(n)(三次线性遍历:计算left、计算right、计算结果),耗时3ms仅击败10.84%用户(主要因额外数组的内存操作开销);
- 空间复杂度:O(n)(left和right数组各占用n个空间),内存消耗63.7MB击败81.69%用户;
- 优势:逻辑清晰,新手易理解,完全规避了除法操作(符合题目要求);
- 可优化点:额外使用两个数组,未利用“输出数组不计入额外空间”的规则,空间复杂度未达进阶要求。
public int[] productExceptSelf(int[] nums) {
int length=nums.length;
int [] left =new int[length];
int [] right=new int[length];
int [] result=new int[length];
left[0]=nums[0];
for(int i=1;i<length;i++){
left[i]=nums[i]*left[i-1];
}
right[length-1]=nums[length-1];
for(int i=length-2;i>=0;i--){
right[i]=nums[i]*right[i+1];
}
for(int i=0;i<length;i++){
if(i==0){
result[i]=right[i+1];
}else if(i==length-1){
result[i]=left[length-2];
}else {
result[i]=left[i-1]*right[i+1];
}
}
return result;
}
示例解答
解题思路
解法1:空间优化版(O(1)额外空间,最优解)
核心方法:输出数组复用 + 单次遍历填充,利用输出数组先存储前缀乘积,再通过反向遍历用变量记录后缀乘积,最终组合得到结果,仅使用常数级额外空间(满足进阶要求)。
核心优化逻辑
- 第一步:输出数组存储前缀乘积:
- 初始化
result[0] = 1(第一个元素左侧无元素,前缀乘积为1); - 遍历数组,
result[i] = result[i-1] × nums[i-1](result[i]表示i位置左侧所有元素的乘积);
- 初始化
- 第二步:反向遍历计算后缀乘积并组合结果:
- 初始化变量
suffix = 1(最后一个元素右侧无元素,后缀乘积为1); - 从后往前遍历,
result[i] = result[i] × suffix(前缀×后缀); - 更新
suffix = suffix × nums[i](将当前元素加入后缀乘积,供前一个位置使用)。
- 初始化变量
代码实现
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] result = new int[n];
// 第一步:计算前缀乘积(存储在result中)
result[0] = 1;
for (int i = 1; i < n; i++) {
result[i] = result[i-1] * nums[i-1];
}
// 第二步:反向计算后缀乘积并组合结果
int suffix = 1;
for (int i = n-1; i >= 0; i--) {
result[i] = result[i] * suffix;
suffix *= nums[i];
}
return result;
}
性能优势
- 时间复杂度:O(n)(两次线性遍历,比原解法少一次遍历),耗时可降至1ms左右(击败99%+用户);
- 额外空间复杂度:O(1)(仅使用
suffix一个变量,输出数组不计入额外空间); - 核心优化点:
- 复用输出数组存储前缀乘积,消除left数组的空间开销;
- 用变量替代right数组,反向遍历过程中动态计算后缀乘积;
- 减少数组内存分配和访问的开销,提升执行效率。
具体步骤(以nums=[1,2,3,4]为例)
- 前缀乘积填充result:
- result[0] = 1;
- result[1] = result[0]×nums[0] = 1×1=1;
- result[2] = result[1]×nums[1] = 1×2=2;
- result[3] = result[2]×nums[2] = 2×3=6;
- 此时result = [1,1,2,6];
- 反向遍历计算后缀:
- i=3:result[3] = 6×1=6,suffix=1×4=4;
- i=2:result[2] = 2×4=8,suffix=4×3=12;
- i=1:result[1] = 1×12=12,suffix=12×2=24;
- i=0:result[0] = 1×24=24,suffix=24×1=24;
- 最终result = [24,12,8,6]。
解法2:分治优化版(兼容大数场景)
核心方法:拆分正负/零的乘积逻辑,针对数组包含0或负数的场景,通过统计0的个数、负数的个数,结合总乘积计算结果(需注意:仅当数组无0时可用此方法,且需确保总乘积不溢出)。
代码实现(补充思路,非最优但拓展思维)
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] result = new int[n];
int totalProduct = 1;
int zeroCount = 0;
// 计算总乘积和0的个数
for (int num : nums) {
if (num == 0) {
zeroCount++;
continue;
}
totalProduct *= num;
}
// 根据0的个数计算结果
for (int i = 0; i < n; i++) {
if (zeroCount > 1) {
// 多个0,所有位置结果都是0
result[i] = 0;
} else if (zeroCount == 1) {
// 一个0,只有0的位置结果为总乘积,其余为0
result[i] = nums[i] == 0 ? totalProduct : 0;
} else {
// 无0,结果=总乘积/当前元素(题目禁止除法,仅作拓展)
result[i] = totalProduct / nums[i];
}
}
return result;
}
适用场景说明
- 该方法仅作思路拓展,题目明确禁止使用除法,因此不推荐作为正式解法;
- 优势:在无0且无溢出的场景下,仅需两次遍历,计算更简洁;
- 局限性:无法处理数组包含0的场景(除法无意义),且大数相乘易溢出,不符合题目要求。
总结
- 左右前缀数组法(第一次解答):逻辑直观,O(n)时间+O(n)空间,适合新手理解核心原理;
- 空间优化版(最优解):O(n)时间+O(1)额外空间,复用输出数组+动态后缀变量,满足进阶要求,工程首选;
- 分治优化版:仅作思路拓展,因使用除法不符合题目要求,实际不可用;
- 关键技巧:
- 核心思想:将“除自身外乘积”拆解为“前缀×后缀”,避免暴力遍历;
- 空间优化:利用“输出数组不计入额外空间”的规则,复用数组存储中间结果;
- 边界处理:注意首尾元素的前缀/后缀为空的情况(乘积为1)。