🎯 题目链接:leetcode.cn/problems/product-of-array-except-self/
🔍 难度:中等 | 🏷️ 标签:数组、前后缀分解、双指针、原地操作
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(不计输出数组)
🧠 题目分析
给你一个整数数组
nums,返回一个新数组answer,其中answer[i]等于nums中 除nums[i]之外其余所有元素的乘积。
❌ 不允许使用除法
⏱️ 要求时间复杂度为 O(n)
💾 进阶要求:空间复杂度 O(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
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
解释:
- index 0: 1*0*(-3)*3 = 0
- index 1: (-1)*0*(-3)*3 = 0
- index 2: (-1)*1*(-3)*3 = 9
- index 3: (-1)*1*0*3 = 0
- index 4: (-1)*1*0*(-3) = 0
⚠️ 关键点:不能用除法! 否则遇到 0 就会出问题。比如 nums = [0, 1, 2],如果用总乘积 / nums[i],那 i=0 时就会出现除以零错误。
🧩 核心算法及代码讲解
✅ 核心思想:前后缀分解(Prefix & Suffix Decomposition)
定义:
- 前缀乘积(Left Product):
pre[i]表示从nums[0]到nums[i-1]的乘积。- 后缀乘积(Right Product):
suf[i]表示从nums[i+1]到nums[n-1]的乘积。则有:
answer[i] = pre[i] * suf[i]
💡 为什么这个方法有效?
因为我们想计算“除了自己之外的所有数的乘积”,而这些数可以分为两部分:
- 左边的所有数(前缀)
- 右边的所有数(后缀)
所以只需分别预处理左右两边的乘积即可。
🔄 优化思路:空间压缩 → O(1)
虽然我们能用两个数组 L 和 R 分别存储前缀和后缀乘积,但这样空间是 O(n)。
👉 进阶技巧:我们可以 先用输出数组 answer 存储前缀乘积,然后在第二次遍历中动态维护一个变量 R 来表示当前右边的乘积。
✅ 此时:
answer[i]先存的是左边乘积(即pre[i])- 再乘上
R(右边乘积),得到最终结果- 每次更新
R *= nums[i],保证下一次迭代时R是新的右乘积
📌 关键洞察:输出数组不算额外空间 → 可复用!
🧠 解题思路(分步详解)
-
初始化输出数组
answer,用于存储前缀乘积(第一遍)answer[0] = 1,因为第一个元素左边没有数
-
第一次遍历(从左到右):
- 计算每个位置的左侧乘积
answer[i] = answer[i-1] * nums[i-1]- 即:
answer[i]是nums[0] ~ nums[i-1]的乘积
-
第二次遍历(从右到左):
- 维护一个变量
R,初始为 1(代表最右边没有元素) - 对每个
i:answer[i] = answer[i] * R(左边 × 右边)- 更新
R = R * nums[i],为下一个位置准备右边乘积
- 维护一个变量
-
返回
answer
✅ 时间复杂度:O(n),两次遍历
✅ 空间复杂度:O(1),仅使用常量级变量(输出数组不计)
📈 算法分析
| 指标 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 两次线性扫描 |
| 空间复杂度 | O(1) | 输出数组不计,仅用一个变量 R |
| 是否可扩展 | ✅ | 可推广至任意“排除某个位置”类问题 |
| 面试价值 | ⭐⭐⭐⭐⭐ | 经典题,考察思维转换能力 |
🧪 代码实现(完整模板)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 核心函数:除自身以外数组的乘积 —— 前后缀分解 + 原地优化
vector<int> productExceptSelf(vector<int>& nums) {
int length = nums.size();
vector<int> answer(length);
// 第一步:计算前缀乘积,存入 answer 数组
// answer[i] 表示 i 左侧所有元素的乘积
answer[0] = 1; // 第一个元素左边无元素,乘积为 1
for (int i = 1; i < length; i++) {
answer[i] = nums[i - 1] * answer[i - 1]; // 当前前缀 = 上一个前缀 × 上一个元素
}
// 第二步:从右往左遍历,同时维护右侧乘积 R
int R = 1; // R 表示当前位置右边所有元素的乘积
for (int i = length - 1; i >= 0; i--) {
// 当前答案 = 左侧乘积 × 右侧乘积
answer[i] = answer[i] * R;
// 更新 R:将当前元素加入右侧乘积,供下一次使用
R *= nums[i];
}
return answer;
}
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 测试用例 1
vector<int> nums1 = {1, 2, 3, 4};
auto result1 = productExceptSelf(nums1);
cout << "Test 1: ";
for (int x : result1) cout << x << " ";
cout << endl;
// 测试用例 2
vector<int> nums2 = {-1, 1, 0, -3, 3};
auto result2 = productExceptSelf(nums2);
cout << "Test 2: ";
for (int x : result2) cout << x << " ";
cout << endl;
return 0;
}
🔍 代码逐行解析(核心逻辑)
answer[0] = 1; // 左边界:第一个元素左边无元素,乘积为 1
for (int i = 1; i < length; i++) {
answer[i] = nums[i - 1] * answer[i - 1]; // 累积左侧乘积
}
➡️ 比如 nums = [1,2,3,4],执行后:
answer = [1, 1, 2, 6]- 分别是:
1,1,1×2,1×2×3
int R = 1;
for (int i = length - 1; i >= 0; i--) {
answer[i] = answer[i] * R; // 左侧 × 右侧
R *= nums[i]; // 更新右侧乘积
}
➡️ 从右到左:
i=3:answer[3] = 6 * 1 = 6,R = 1 * 4 = 4i=2:answer[2] = 2 * 4 = 8,R = 4 * 3 = 12i=1:answer[1] = 1 * 12 = 12,R = 12 * 2 = 24i=0:answer[0] = 1 * 24 = 24,R = 24 * 1 = 24
最终:answer = [24, 12, 8, 6] ✅
💡 面试加分点
✅ 为什么不用除法?
- 如果数组中有多个 0,则总乘积为 0,无法通过
total / nums[i]得到正确结果 - 例如
[0, 0, 1]:total = 0,answer[0] = 0/0错误!
✅ 如何处理多个 0?
本题不需要显式处理,因为:
- 如果只有一个 0,那么只有那个位置的答案非零(等于其他数乘积)
- 如果有两个或以上 0,则所有位置答案都为 0
📌 所以我们不需要特殊判断,前后缀分解天然支持这种情况
✅ 前后缀分解的通用性
这种模式适用于很多“排除某位置”的问题,例如:
| 问题 | 思路 |
|---|---|
除自身外的乘积 | 左右乘积相乘 |
最大子数组乘积 | 类似动态规划,但需考虑正负 |
滑动窗口最大值 | 单调队列 |
数组中每个元素的下一个更大元素 | 单调栈 |
✅ 掌握“前后缀分解”是解决这类问题的关键!
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第17题 —— 41.缺失的第一个正数(困难)
🔹 题目:给定一个未排序整数数组,找出其中最小的缺失正整数。
🔹 核心思路:利用数组本身作为哈希表,通过索引映射进行标记。
🔹 考点:原地修改、索引映射、数学技巧、空间优化。
🔹 难度:困难,但思想极为巧妙,是“原地哈希”的经典应用!
💡 提示:不要用额外哈希表!目标是 O(1) 空间 + O(n) 时间!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!
🎯 坚持每日一题,算法不再难!
📚 专栏持续更新中…