题目描述(题目链接):
给定一个长度为 n的整数数组 nums,要求返回一个数组 answer,其中 answeri 等于 nums 中除了 numsi 之外其余各元素的乘积。
严苛的限制条件:
- 禁止使用除法:不能先求出总乘积再除以当前元素。
- 时间复杂度O(n):不能使用嵌套循环暴力求解。
- 进阶空间要求 O(1):除了输出数组外,不能申请额外的与 n相关的空间。
面对这些限制,传统的暴力解法(双重循环)和除法解法(需处理 0 和精度问题)均失效。我们需要一种更巧妙的前缀与后缀思想。
核心算法思想:前缀积 × 后缀积
对于数组中的任意一个位置 i,我们要求的 answeri 实际上可以拆解为两部分:

即:当前位置左侧所有元素的乘积(前缀积) 乘以 当前位置右侧所有元素的乘积(后缀积)。
算法设计步骤
为了满足 O(1)的额外空间复杂度(不计算输出数组),我们可以复用输出数组 answer 来存储中间结果,并通过两次遍历完成计算:
- 第一次遍历(从左到右) :
计算每个位置 i 的左侧乘积 ,并直接存入 answeri 中。此时 answeri 表示 numsi 左边所有元素的乘积。
- answer0 = 1(第一个元素左边无元素,乘积为 1)。
- answeri = answeri-1 * numsi-1。
- 第二次遍历(从右到左) :
使用一个变量 R 动态维护当前位置右侧所有元素的乘积 (初始 R = 1)。在遍历过程中,将 R 乘入 answeri,然后更新 R。
- answeri = answeri * R(此时 answeri 变为左侧乘积 × 右侧乘积)。
- R = R * numsi(更新 R,为下一个左侧元素做准备)。
代码实现 (C++)
以下是符合题目进阶空间要求的最优 C++ 实现:
cpp
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> answer(n);
// 第一步:计算左侧乘积并存入 answer
// answer[i] 表示 nums[i] 左侧所有元素的乘积
answer[0] = 1;
for (int i = 1; i < n; i++) {
answer[i] = answer[i - 1] * nums[i - 1];
}
// 第二步:计算右侧乘积并直接与左侧乘积相乘
int R = 1; // R 动态维护右侧元素的乘积
for (int i = n - 1; i >= 0; i--) {
answer[i] = answer[i] * R; // 左侧乘积 * 右侧乘积
R = R * nums[i]; // 更新 R,供下一个(左侧)元素使用
}
return answer;
}
};

算法复杂度分析
时间复杂度: O(n)
我们进行了两次遍历,每次遍历处理 n 个元素,总操作数为 2n,渐进时间复杂度为线性阶 O(n)。
空间复杂度: O(1)
除了必须返回的 answer 数组外,我们只使用了常数个变量(R 和循环索引 i),没有开辟额外的数组空间。这完美满足了题目进阶的 O(1)空间复杂度要求。
示例推演
以 nums = 1, 2, 3, 4 为例:
第一次遍历(计算左侧积):
i=0: answer0 = 1
i=1: answer1 = 1 * 1 = 1
i=2: answer2 = 1 * 2 = 2
i=3: answer3 = 2 * 3 = 6
此时 answer = 1, 1, 2, 6
第二次遍历(从右向左乘入右侧积):
初始 R = 1
i=3: answer3 = 6 * 1 = 6;R = 1 * 4 = 4
i=2: answer2 = 2 * 4 = 8;R = 4 * 3 = 12
i=1: answer1 = 1 * 12 = 12;R = 12 * 2 = 24
i=0: answer0 = 1 * 24 = 24;R = 24 * 1 = 24
最终输出: 24, 12, 8, 6,与预期结果完全一致。