题目描述
给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
题目数据保证数组 nums 之中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内。
请不要使用除法 ,且在 O(n) 时间复杂度内完成此题。
示例 1:
输入:nums = [1,2,3,4]
输出:[24,12,8,6]
解释:answer[0] = 2*3*4 = 24, answer[1] = 1*3*4 = 12, answer[2] = 1*2*4 = 8, answer[3] = 1*2*3 = 6
示例 2:
输入:nums = [-1,1,0,-3,3]
输出:[0,0,9,0,0]
提示:
-
2 <= nums.length <= 10^5 -
-30 <= nums[i] <= 30 -
保证数组
nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内
进阶: 你可以在 O(1) 的额外空间复杂度内完成这个题目吗?(出于对空间复杂度分析的目的,输出数组不被视为额外空间)
解题思路
这道题的难点在于不能使用除法 ,且要求 O(n) 时间复杂度。最直观的想法是计算所有元素的乘积,然后除以当前元素,但这种方法不仅被题目禁止,还需要特殊处理数组中存在 0 的情况,非常麻烦。
核心思想:左右乘积列表
对于每个位置 i,最终结果可以分解为:
answer[i] = (nums[0] × nums[1] × ... × nums[i-1]) × (nums[i+1] × ... × nums[n-1])
\_________________________/ \_________________________/
左乘积 右乘积
因此,我们只需要预先计算出每个位置左侧所有元素的乘积和右侧所有元素的乘积,然后将两者相乘即可。
解法一:左右乘积数组(直观解法)
算法步骤
-
初始化两个数组
L和R,长度与nums相同 -
L[i]表示nums[i]左侧所有元素的乘积,L[0] = 1(第一个元素左边没有元素) -
R[i]表示nums[i]右侧所有元素的乘积,R[n-1] = 1(最后一个元素右边没有元素) -
从左到右遍历,计算
L数组:L[i] = L[i-1] × nums[i-1] -
从右到左遍历,计算
R数组:R[i] = R[i+1] × nums[i+1] -
最终结果
answer[i] = L[i] × R[i]
代码实现
java
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] L = new int[n]; // 左侧乘积
int[] R = new int[n]; // 右侧乘积
int[] answer = new int[n];
// 计算左侧乘积
L[0] = 1; // 第一个元素左边没有元素
for (int i = 1; i < n; i++) {
L[i] = L[i - 1] * nums[i - 1];
}
// 计算右侧乘积
R[n - 1] = 1; // 最后一个元素右边没有元素
for (int i = n - 2; i >= 0; i--) {
R[i] = R[i + 1] * nums[i + 1];
}
// 计算结果
for (int i = 0; i < n; i++) {
answer[i] = L[i] * R[i];
}
return answer;
}
}
图解示例
以 nums = [1, 2, 3, 4] 为例:
| 索引 i | nums[i] | L[i](左侧乘积) | R[i](右侧乘积) | answer[i] = L[i] × R[i] |
|---|---|---|---|---|
| 0 | 1 | 1 | 2×3×4 = 24 | 1 × 24 = 24 |
| 1 | 2 | 1×1 = 1 | 3×4 = 12 | 1 × 12 = 12 |
| 2 | 3 | 1×1×2 = 2 | 4 = 4 | 2 × 4 = 8 |
| 3 | 4 | 1×1×2×3 = 6 | 1 | 6 × 1 = 6 |
最终结果: [24, 12, 8, 6]
复杂度分析
-
时间复杂度: O(n),需要三次遍历(计算L、计算R、计算结果)
-
空间复杂度: O(n),使用了两个额外数组 L 和 R
解法二:空间优化 O(1) 【最优解】
优化思路
题目进阶要求 O(1) 的额外空间复杂度。注意:输出数组不计入空间复杂度,因此我们可以利用输出数组来存储中间结果。
具体做法:
-
先将
answer数组当作L数组使用,存储每个位置左侧的乘积 -
然后从右向左遍历,用一个变量
R动态维护右侧乘积 -
将
R直接乘到answer[i]上,即可得到最终结果
代码实现
java
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] answer = new int[n];
// 1. 计算左侧乘积,存入 answer 数组
answer[0] = 1; // 第一个元素左边没有元素
for (int i = 1; i < n; i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}
// 2. 从右向左遍历,动态维护右侧乘积并乘到 answer 上
int R = 1; // 右侧乘积初始值(最右边元素的右侧乘积为1)
for (int i = n - 1; i >= 0; i--) {
// answer[i] 目前存储的是左侧乘积,乘上右侧乘积得到最终结果
answer[i] = answer[i] * R;
// 更新右侧乘积,为下一个左边的元素准备
R = R * nums[i];
}
return answer;
}
}
详细执行过程
以 nums = [1, 2, 3, 4] 为例:
第一步:从左到右计算左侧乘积(存入 answer)
| i | answer[i] 计算过程 | answer 数组状态 |
|---|---|---|
| 0 | 初始化 answer[0] = 1 | [1, 0, 0, 0] |
| 1 | answer[1] = answer[0] × nums[0] = 1 × 1 = 1 | [1, 1, 0, 0] |
| 2 | answer[2] = answer[1] × nums[1] = 1 × 2 = 2 | [1, 1, 2, 0] |
| 3 | answer[3] = answer[2] × nums[2] = 2 × 3 = 6 | [1, 1, 2, 6] |
此时 answer = [1, 1, 2, 6] 存储的是每个位置左侧的乘积。
第二步:从右到左计算右侧乘积并更新 answer
| i | R 更新前 | answer[i] 更新前 | 更新 answer[i] | R 更新后 |
|---|---|---|---|---|
| 3 | R = 1 | answer[3] = 6 | answer[3] = 6 × 1 = 6 | R = 1 × nums[3] = 1 × 4 = 4 |
| 2 | R = 4 | answer[2] = 2 | answer[2] = 2 × 4 = 8 | R = 4 × nums[2] = 4 × 3 = 12 |
| 1 | R = 12 | answer[1] = 1 | answer[1] = 1 × 12 = 12 | R = 12 × nums[1] = 12 × 2 = 24 |
| 0 | R = 24 | answer[0] = 1 | answer[0] = 1 × 24 = 24 | R = 24 × nums[0] = 24 × 1 = 24 |
最终结果: answer = [24, 12, 8, 6]
复杂度分析
-
时间复杂度: O(n),只需两次遍历
-
空间复杂度: O(1),只使用了常数个额外变量(R),输出数组不计入空间复杂度
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 左右乘积数组 | O(n) | O(n) | 思路直观,容易理解 | 使用了额外空间 |
| 空间优化法 | O(n) | O(1) | 满足进阶要求,最优解 | 需要理解复用数组的技巧 |
常见误区
误区一:想用除法
java
// ❌ 错误思路:计算总乘积后除以当前元素
int total = 1;
for (int num : nums) total *= num;
for (int i = 0; i < n; i++) {
answer[i] = total / nums[i]; // 禁止使用除法 + 当nums[i]=0时会出错
}
问题:
-
题目明确禁止使用除法
-
当数组中有 0 时,总乘积为 0,除以 0 会出错
-
如果有多个 0,结果数组应该全是 0,但这种方法无法正确处理
误区二:暴力嵌套循环
java
// ❌ 错误思路:对每个位置重新计算乘积
for (int i = 0; i < n; i++) {
int product = 1;
for (int j = 0; j < n; j++) {
if (j != i) product *= nums[j];
}
answer[i] = product;
}
问题: 时间复杂度 O(n²),当 n = 10^5 时会严重超时。
注意事项
-
初始化值必须为 1:乘积的初始值必须是 1,不能是 0
-
边界处理:第一个元素的左侧乘积为 1,最后一个元素的右侧乘积为 1
-
遍历顺序:计算左侧乘积从左到右,计算右侧乘积从右到左
-
变量复用 :在空间优化解法中,
R变量要正确更新,顺序很重要
面试建议
-
优先写出空间优化解法:这是最符合题目进阶要求的解法,展现你对空间复杂度的理解
-
可以提及左右乘积数组法:作为对比,说明你知道多种解法
-
解释为什么不能用除法:展示你考虑了边界情况(数组中存在 0 的情况)
-
画图演示 :面试时可以用简单的例子(如
[1,2,3,4])演示算法的执行过程
相关题目推荐
-
力扣 152. 乘积最大子数组
-
力扣 53. 最大子数组和
-
力扣 724. 寻找数组的中心下标
-
力扣 1991. 找到数组的中间位置
以上就是力扣 238 题"除自身以外数组的乘积"的 Java 解法详细解析,重点掌握空间优化解法,这是面试中的常考题。如果觉得文章不错,欢迎点赞、收藏、关注三连支持!