一、题目核心拆解(必看)
先抛开专业术语,用大白话把题目说透,确保你完全理解需求:
给定一个整数数组(比如 [1,2,3,4]),要求返回一个新数组(answer),新数组中每个位置的数(answer[i]),等于原数组中"除了当前位置的数(nums[i])之外,所有其他数的乘积"。
举2个具体例子,一看就懂:
示例1:输入 nums = [1,2,3,4]
-
answer[0] = 2×3×4 = 24(除了nums[0]=1,其他数相乘)
-
answer[1] = 1×3×4 = 12(除了nums[1]=2,其他数相乘)
-
answer[2] = 1×2×4 = 8(除了nums[2]=3,其他数相乘)
-
answer[3] = 1×2×3 = 6(除了nums[3]=4,其他数相乘)
所以输出是 [24,12,8,6]
示例2:输入 nums = [-1,1,0,-3,3]
-
answer[0] = 1×0×(-3)×3 = 0(除了nums[0]=-1,其他数有0,乘积为0)
-
answer[1] = (-1)×0×(-3)×3 = 0(除了nums[1]=1,其他数有0)
-
answer[2] = (-1)×1×(-3)×3 = 9(除了nums[2]=0,其他数相乘)
-
answer[3] = (-1)×1×0×3 = 0(除了nums[3]=-3,其他数有0)
-
answer[4] = (-1)×1×0×(-3) = 0(除了nums[4]=3,其他数有0)
所以输出是 [0,0,9,0,0]
题目强制要求(重点!):
-
不能用除法(比如先算所有数的总乘积,再除以每个数------不行!因为数组里可能有0,除以0会报错,而且题目明确禁止);
-
时间复杂度必须是 O(n)(简单说:只能遍历数组1~3次,不能嵌套遍历,否则会超时);
-
进阶要求(加分项):额外空间复杂度 O(1)(简单说:除了存储最终答案的数组,不能再开和原数组长度一样的额外数组,只能用几个变量)。
二、必备前置知识
在看解法之前,必须先掌握这5个基础知识点,否则看代码会懵------我会用最通俗的语言,配具体例子,确保完全不懂编程的人也能理解。
2.1 什么是数组?
数组就是一个"装数字的容器",里面的每个数字都有一个唯一的"位置编号"(专业叫"索引"),索引从0开始(不是从1!这是最容易踩的坑)。
例子:nums = [1,2,3,4]
-
索引0 → 数字1(第一个数,索引是0)
-
索引1 → 数字2(第二个数,索引是1)
-
索引2 → 数字3(第三个数,索引是2)
-
索引3 → 数字4(第四个数,索引是3)
我们可以通过"索引"快速找到对应的数字,比如 nums[0] 就是1,nums[2] 就是3。
2.2 数组的遍历(核心操作)
遍历就是"逐个访问数组里的每个数字",常见两种方式,和解法密切相关:
- 正序遍历:从索引0开始,依次访问到最后一个索引(n-1,n是数组长度);
例子:nums = [1,2,3,4],正序遍历顺序是:nums[0](1)→ nums[1](2)→ nums[2](3)→ nums[3](4)。
- 倒序遍历:从最后一个索引(n-1)开始,依次访问到索引0;
例子:nums = [1,2,3,4],倒序遍历顺序是:nums[3](4)→ nums[2](3)→ nums[1](2)→ nums[0](1)。
2.3 前缀乘积 & 后缀乘积(解法核心技巧)
这两个是本题的"灵魂",一定要吃透,用具体例子讲透:
- 前缀乘积(左乘积):对于数组中某个位置i,"前缀乘积"就是"i左边所有数字的乘积";
例子:nums = [1,2,3,4]
-
索引0:左边没有数字 → 前缀乘积 = 1(任何数乘1都不变,方便后续计算);
-
索引1:左边只有数字1 → 前缀乘积 = 1;
-
索引2:左边有数字1、2 → 前缀乘积 = 1×2 = 2;
-
索引3:左边有数字1、2、3 → 前缀乘积 = 1×2×3 = 6。
所以,前缀乘积数组(left)就是 [1,1,2,6]。
- 后缀乘积(右乘积):对于数组中某个位置i,"后缀乘积"就是"i右边所有数字的乘积";
例子:nums = [1,2,3,4]
-
索引3:右边没有数字 → 后缀乘积 = 1;
-
索引2:右边只有数字4 → 后缀乘积 = 4;
-
索引1:右边有数字3、4 → 后缀乘积 = 3×4 = 12;
-
索引0:右边有数字2、3、4 → 后缀乘积 = 2×3×4 = 24。
所以,后缀乘积数组(right)就是 [24,12,4,1]。
关键结论:answer[i] = 前缀乘积[i] × 后缀乘积[i](比如索引0:1×24=24,索引1:1×12=12,和示例1完全对应)。
2.4 时间复杂度 & 空间复杂度(题目强制要求)
不用记复杂公式,用大白话+例子讲懂:
- 时间复杂度:衡量"算法执行快慢",用 O(...) 表示,核心看"遍历次数";
-
O(n):只遍历数组1~3次(n是数组长度),效率极高;比如遍历一次数组,执行n次操作,就是O(n);
-
O(n²):嵌套遍历(比如外层遍历n次,内层也遍历n次),执行n×n次操作,效率极低;本题n最大是100000,O(n²)会执行10000000000次操作,电脑处理不了,会"超时"(无法通过测试)。
- 空间复杂度:衡量"算法占用内存多少",用 O(...) 表示,核心看"是否开额外数组";
-
O(1):只用到几个变量(比如int a=1,没有开和原数组长度一样的额外数组);
-
O(n):开了一个或多个"和原数组长度相同"的额外数组(比如前缀乘积数组left,长度和nums一样,就是O(n));
注意:题目说"输出数组不算额外空间",所以存储答案的数组(answer),不算在空间复杂度里。
2.5 为什么不能用除法?(补充说明)
很多会想:"先算所有数的总乘积,再除以每个数,不就得到答案了?"------不行!原因有2个:
-
数组中可能有0:除以0是数学错误,程序会报错;
-
题目明确禁止用除法(考察的是"前缀/后缀乘积"的思路,不是除法技巧)。
三、解法一:暴力解法
先从最直观的思路入手,虽然会超时,但能帮你理解"为什么需要优化",也能轻松看懂。
3.1 核心思路(大白话)
对数组中每个位置i,都重新遍历一次数组,把"除了nums[i]之外的所有数"乘起来,存到answer[i]里。
简单说:双重循环,外层循环找"当前要计算的位置i",内层循环找"除了i之外的所有数",相乘即可。
3.2 Python代码(逐句注释,能看懂每一步)
python
from typing import List # 导入List类型,告诉电脑:nums是整数列表,返回值也是整数列表(可忽略,按模板写即可)
class Solution: # 定义一个"解决方案类",算法题的固定写法,用于封装解题函数
def productExceptSelf(self, nums: List[int]) -> List[int]: # 定义解题函数,nums是输入数组,返回answer数组
n = len(nums) # 计算数组长度n,比如nums=[1,2,3,4],n=4
answer = [1] * n # 初始化答案数组,所有元素为1(乘1不影响结果,方便后续相乘)
# 外层循环:遍历每个位置i,计算answer[i](i从0到n-1)
for i in range(n):
# 内层循环:遍历每个位置j,找"不是i"的位置,把nums[j]乘到answer[i]里
for j in range(n):
if i != j: # 只要j不是i,就把nums[j]乘进去(排除当前元素)
answer[i] *= nums[j] # 等价于:answer[i] = answer[i] * nums[j]
return answer # 返回最终的答案数组
# 测试主函数:用于验证代码正确性,可直接运行,包含题目示例+自定义输入
def main():
# 1. 测试题目示例1
print("测试题目示例1:")
nums1 = [1,2,3,4] # 题目给的输入
sol = Solution() # 创建Solution类的对象sol(用于调用解题函数)
res1 = sol.productExceptSelf(nums1) # 调用解题函数,传入nums1,得到结果res1
print(f"输入数组:{nums1}")
print(f"输出结果:{res1}") # 预期输出:[24,12,8,6]
print("-" * 50) # 分隔线,美化输出
# 2. 测试题目示例2
print("测试题目示例2:")
nums2 = [-1,1,0,-3,3] # 题目给的输入
res2 = sol.productExceptSelf(nums2) # 调用解题函数
print(f"输入数组:{nums2}")
print(f"输出结果:{res2}") # 预期输出:[0,0,9,0,0]
print("-" * 50)
# 3. 自定义输入测试(可自己改输入,验证代码)
print("自定义输入测试:")
# 获取用户输入:比如输入"2 3 4 5",转换为整数列表
input_str = input("请输入数组元素(空格分隔):")
nums3 = list(map(int, input_str.split())) # 把输入的字符串转换成整数列表
res3 = sol.productExceptSelf(nums3) # 调用解题函数
print(f"输入数组:{nums3}")
print(f"输出结果:{res3}")
# 执行测试主函数(固定写法,直接复制即可)
if __name__ == "__main__":
main()
3.3 C++代码(逐句注释,能看懂每一步)
cpp
#include <vector> // 导入vector(C++的动态数组)头文件,用于存储数组
#include <iostream> // 导入输入输出头文件,用于实现用户输入和结果输出
using namespace std; // 使用标准命名空间,避免每次写cout、cin都加std::
// 定义解决方案类,封装解题函数(算法题固定写法)
class Solution {
public:
// 定义解题函数:参数nums是vector<int>类型(整数数组),返回值也是vector<int>类型(答案数组)
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size(); // 计算数组长度n,size()是vector的方法,获取元素个数
vector<int> answer(n, 1); // 初始化答案数组,长度为n,所有元素初始化为1
// 外层循环:遍历每个位置i(0到n-1),计算answer[i]
for (int i = 0; i < n; i++) {
// 内层循环:遍历每个位置j(0到n-1),排除i,相乘
for (int j = 0; j < n; j++) {
if (i != j) { // 只乘"不是当前位置i"的元素
answer[i] *= nums[j]; // 累积相乘,更新answer[i]
}
}
}
return answer; // 返回答案数组
}
};
// 测试主函数:验证代码正确性,包含示例+自定义输入,可直接运行
int main() {
// 1. 测试题目示例1
cout << "测试题目示例1:" << endl;
vector<int> nums1 = {1, 2, 3, 4}; // 题目给的输入数组
Solution sol; // 创建Solution类的对象sol,用于调用解题函数
vector<int> res1 = sol.productExceptSelf(nums1); // 调用解题函数,获取结果
cout << "输入数组:";
for (int num : nums1) { // 遍历输入数组,输出内容(C++11及以上支持的遍历方式)
cout << num << " ";
}
cout << endl << "输出结果:";
for (int num : res1) { // 遍历结果数组,输出内容
cout << num << " ";
}
cout << endl << "------------------------" << endl; // 分隔线
// 2. 测试题目示例2
cout << "测试题目示例2:" << endl;
vector<int> nums2 = {-1, 1, 0, -3, 3}; // 题目给的输入
vector<int> res2 = sol.productExceptSelf(nums2); // 调用解题函数
cout << "输入数组:";
for (int num : nums2) {
cout << num << " ";
}
cout << endl << "输出结果:";
for (int num : res2) {
cout << num << " ";
}
cout << endl << "------------------------" << endl;
// 3. 自定义输入测试(可自己输入,验证代码)
cout << "自定义输入测试:" << endl;
int n; // 存储用户输入的数组长度
cout << "请输入数组长度:";
cin >> n; // 获取用户输入的数组长度
vector<int> nums3(n); // 定义长度为n的数组
cout << "请输入" << n << "个整数(空格分隔):";
for (int i = 0; i < n; i++) { // 遍历数组,获取用户输入的每个元素
cin >> nums3[i];
}
vector<int> res3 = sol.productExceptSelf(nums3); // 调用解题函数
cout << "输入数组:";
for (int num : nums3) {
cout << num << " ";
}
cout << endl << "输出结果:";
for (int num : res3) {
cout << num << " ";
}
cout << endl;
return 0; // 主函数正常结束,返回0(固定写法)
}
3.4 代码运行过程(必看,以示例1为例)
输入 nums = [1,2,3,4],n=4,answer初始为 [1,1,1,1]
- 当i=0(计算answer[0]):
j=0:i==j,跳过;j=1:i!=j,answer[0] = 1×2=2;j=2:answer[0] = 2×3=6;j=3:answer[0] =6×4=24 → answer变成 [24,1,1,1]
- 当i=1(计算answer[1]):
j=0:i!=j,answer[1] =1×1=1;j=1:跳过;j=2:answer[1] =1×3=3;j=3:answer[1] =3×4=12 → answer变成 [24,12,1,1]
- 当i=2(计算answer[2]):
j=0:1×1=1;j=1:1×2=2;j=2:跳过;j=3:2×4=8 → answer变成 [24,12,8,1]
- 当i=3(计算answer[3]):
j=0:1×1=1;j=1:1×2=2;j=2:2×3=6;j=3:跳过 → answer变成 [24,12,8,6]
最终返回 [24,12,8,6],和示例一致,但效率极低。
3.5 缺点(为什么超时?)
时间复杂度是 O(n²),当n=100000时,要执行100000×100000=10000000000次操作,电脑处理不了,会"超时",无法通过题目测试,所以这个解法只能用来理解思路,不能提交。
四、解法二:左右乘积数组(易理解,O(n)空间)
这是最容易理解的"合格解法",解决了暴力解法的超时问题,思路就是我们前置知识里讲的"前缀乘积+后缀乘积",适合入门。
4.1 核心思路(大白话)
-
开一个"前缀乘积数组left",存储每个位置i的"左边所有数的乘积";
-
开一个"后缀乘积数组right",存储每个位置i的"右边所有数的乘积";
-
答案数组answer[i] = left[i] × right[i];
这样只需要遍历3次数组(left一次、right一次、answer一次),时间复杂度O(n),不会超时。
4.2 Python代码(逐句注释,含测试主函数)
python
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums) # 1. 计算数组长度n
# 2. 初始化前缀乘积数组left、后缀乘积数组right、答案数组answer,均为长度n,初始值1
left = [1] * n # left[i]:nums[i]左边所有数的乘积
right = [1] * n # right[i]:nums[i]右边所有数的乘积
answer = [1] * n # 存储最终答案
# 3. 计算前缀乘积数组left(正序遍历,从索引1开始)
# 原因:索引0左边没有数,left[0]固定为1,不需要计算
for i in range(1, n):
# 关键公式:left[i] = 前一个位置的前缀乘积 × 前一个位置的数字
# 比如i=1:left[1] = left[0] × nums[0] = 1×1=1(左边只有nums[0])
# 比如i=2:left[2] = left[1] × nums[1] = 1×2=2(左边有nums[0]、nums[1])
left[i] = left[i-1] * nums[i-1]
# 4. 计算后缀乘积数组right(倒序遍历,从索引n-2开始)
# 原因:索引n-1右边没有数,right[n-1]固定为1,不需要计算
for i in range(n-2, -1, -1):
# 关键公式:right[i] = 后一个位置的后缀乘积 × 后一个位置的数字
# 比如n=4,i=2:right[2] = right[3] × nums[3] = 1×4=4(右边只有nums[3])
# 比如i=1:right[1] = right[2] × nums[2] = 4×3=12(右边有nums[2]、nums[3])
right[i] = right[i+1] * nums[i+1]
# 5. 计算答案数组:每个位置的前缀乘积 × 后缀乘积
for i in range(n):
answer[i] = left[i] * right[i]
return answer # 返回答案
# 测试主函数(和暴力解法一致,可直接运行,验证正确性)
def main():
# 1. 测试题目示例1
print("测试题目示例1:")
nums1 = [1,2,3,4]
sol = Solution()
res1 = sol.productExceptSelf(nums1)
print(f"输入数组:{nums1}")
print(f"输出结果:{res1}") # 预期[24,12,8,6]
print("-" * 50)
# 2. 测试题目示例2
print("测试题目示例2:")
nums2 = [-1,1,0,-3,3]
res2 = sol.productExceptSelf(nums2)
print(f"输入数组:{nums2}")
print(f"输出结果:{res2}") # 预期[0,0,9,0,0]
print("-" * 50)
# 3. 自定义输入测试
print("自定义输入测试:")
input_str = input("请输入数组元素(空格分隔):")
nums3 = list(map(int, input_str.split()))
res3 = sol.productExceptSelf(nums3)
print(f"输入数组:{nums3}")
print(f"输出结果:{res3}")
if __name__ == "__main__":
main()
4.3 C++代码(逐句注释,含测试主函数)
cpp
#include <vector>
#include <iostream>
using namespace std;
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size(); // 计算数组长度n
// 初始化前缀、后缀、答案数组,长度n,初始值1
vector<int> left(n, 1);
vector<int> right(n, 1);
vector<int> answer(n);
// 计算前缀乘积left:正序遍历(i从1到n-1)
for (int i = 1; i < n; i++) {
// left[i] = 前一个位置的前缀乘积 × 前一个位置的数字
left[i] = left[i-1] * nums[i-1];
}
// 计算后缀乘积right:倒序遍历(i从n-2到0)
for (int i = n-2; i >= 0; i--) {
// right[i] = 后一个位置的后缀乘积 × 后一个位置的数字
right[i] = right[i+1] * nums[i+1];
}
// 计算答案:前缀×后缀
for (int i = 0; i < n; i++) {
answer[i] = left[i] * right[i];
}
return answer;
}
};
// 测试主函数(可直接运行)
int main() {
// 测试示例1
cout << "测试题目示例1:" << endl;
vector<int> nums1 = {1,2,3,4};
Solution sol;
vector<int> res1 = sol.productExceptSelf(nums1);
cout << "输入数组:";
for (int num : nums1) cout << num << " ";
cout << endl << "输出结果:";
for (int num : res1) cout << num << " ";
cout << endl << "------------------------" << endl;
// 测试示例2
cout << "测试题目示例2:" << endl;
vector<int> nums2 = {-1,1,0,-3,3};
vector<int> res2 = sol.productExceptSelf(nums2);
cout << "输入数组:";
for (int num : nums2) cout << num << " ";
cout << endl << "输出结果:";
for (int num : res2) cout << num << " ";
cout << endl << "------------------------" << endl;
// 自定义输入测试
cout << "自定义输入测试:" << endl;
int n;
cout << "请输入数组长度:";
cin >> n;
vector<int> nums3(n);
cout << "请输入" << n << "个整数(空格分隔):";
for (int i = 0; i < n; i++) {
cin >> nums3[i];
}
vector<int> res3 = sol.productExceptSelf(nums3);
cout << "输入数组:";
for (int num : nums3) cout << num << " ";
cout << endl << "输出结果:";
for (int num : res3) cout << num << " ";
cout << endl;
return 0;
}
4.4 代码运行过程(必看,以示例1为例)
输入 nums = [1,2,3,4],n=4,left、right、answer初始均为 [1,1,1,1]
第一步:计算前缀乘积数组left(正序遍历,i从1到3)
-
i=1:left[1] = left[0] × nums[0] = 1×1 = 1 → left变成 [1,1,1,1]
-
i=2:left[2] = left[1] × nums[1] = 1×2 = 2 → left变成 [1,1,2,1]
-
i=3:left[3] = left[2] × nums[2] = 2×3 = 6 → left最终为 [1,1,2,6]
第二步:计算后缀乘积数组right(倒序遍历,i从2到0)
-
i=2:right[2] = right[3] × nums[3] = 1×4 = 4 → right变成 [1,1,4,1]
-
i=1:right[1] = right[2] × nums[2] = 4×3 = 12 → right变成 [1,12,4,1]
-
i=0:right[0] = right[1] × nums[1] = 12×2 = 24 → right最终为 [24,12,4,1]
第三步:计算答案数组answer(遍历所有i)
-
i=0:answer[0] = left[0] × right[0] = 1×24 = 24
-
i=1:answer[1] = left[1] × right[1] = 1×12 = 12
-
i=2:answer[2] = left[2] × right[2] = 2×4 = 8
-
i=3:answer[3] = left[3] × right[3] = 6×1 = 6
最终answer = [24,12,8,6],和示例1一致,且只遍历3次数组,效率极高。
4.5 复杂度分析(能懂)
-
时间复杂度:O(n) → 总共遍历3次数组(left一次、right一次、answer一次),每次遍历n个元素,总操作次数是3n,属于O(n)(常数倍不影响复杂度);
-
空间复杂度:O(n) → 额外开了两个长度为n的数组(left和right),符合题目基本要求,但不符合进阶的O(1)空间要求,所以我们需要进一步优化。
五、解法三:原地优化(进阶最优,O(1)额外空间)
这是本题的"最优解",在解法二的基础上优化空间,去掉额外的left和right数组,只用答案数组和一个变量,完美满足题目进阶要求(O(1)额外空间+O(n)时间),也是面试中最常考的写法。
5.1 核心思路(大白话,结合解法二优化)
解法二的缺点是"开了两个额外数组",我们可以利用"答案数组不算额外空间"的规则,做两个优化:
-
不用单独开left数组:把"前缀乘积"直接存在answer数组里(answer先当left用);
-
不用单独开right数组:用一个变量(suffix)动态计算"后缀乘积",倒序遍历的时候,直接用这个变量和answer里的前缀乘积相乘,得到最终答案。
简单说:answer先存前缀乘积,再用一个变量存动态后缀乘积,一次倒序遍历完成最终计算,省掉两个额外数组。
5.2 Python代码(逐句注释,含测试主函数)
python
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums) # 1. 计算数组长度n
answer = [1] * n # 2. 初始化答案数组,先用来存前缀乘积(替代left数组)
# 3. 第一步:正序遍历,计算前缀乘积,存入answer(此时answer等价于解法二的left数组)
for i in range(1, n):
# 公式和解法二一致:answer[i] = 前一个位置的前缀乘积 × 前一个位置的数字
# 此时answer[i]就是nums[i]左边所有数的乘积
answer[i] = answer[i-1] * nums[i-1]
# 4. 第二步:倒序遍历,用变量动态计算后缀乘积,更新answer为最终结果
suffix = 1 # 定义suffix变量,存储当前位置的后缀乘积(替代right数组),初始为1(最右边元素后缀乘积为1)
for i in range(n-1, -1, -1):
# 核心:当前answer[i]是前缀乘积,乘上suffix(后缀乘积),就是最终答案
answer[i] *= suffix
# 更新suffix:把当前nums[i]加入后缀乘积,供左边的元素使用
# 比如i=3(最右边):suffix初始为1,answer[3] = 6×1=6;之后suffix = 1×4=4(供i=2使用)
suffix *= nums[i]
return answer # 返回最终答案
# 测试主函数(和前面一致,可直接运行,验证正确性)
def main():
# 1. 测试题目示例1
print("测试题目示例1:")
nums1 = [1,2,3,4]
sol = Solution()
res1 = sol.productExceptSelf(nums1)
print(f"输入数组:{nums1}")
print(f"输出结果:{res1}") # 预期[24,12,8,6]
print("-" * 50)
# 2. 测试题目示例2
print("测试题目示例2:")
nums2 = [-1,1,0,-3,3]
res2 = sol.productExceptSelf(nums2)
print(f"输入数组:{nums2}")
print(f"输出结果:{res2}") # 预期[0,0,9,0,0]
print("-" * 50)
# 3. 自定义输入测试
print("自定义输入测试:")
input_str = input("请输入数组元素(空格分隔):")
nums3 = list(map(int, input_str.split()))
res3 = sol.productExceptSelf(nums3)
print(f"输入数组:{nums3}")
print(f"输出结果:{res3}")
if __name__ == "__main__":
main()
5.3 C++代码(逐句注释,含测试主函数)
cpp
#include <vector>
#include <iostream>
using namespace std;
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size(); // 计算数组长度n
vector<int> answer(n, 1); // 初始化答案数组,先存前缀乘积
// 第一步:正序遍历,计算前缀乘积,存入answer(替代left数组)
for (int i = 1; i < n; i++) {
answer[i] = answer[i-1] * nums[i-1];
}
// 第二步:倒序遍历,用suffix变量动态计算后缀乘积,更新答案
int suffix = 1; // 动态后缀乘积变量,初始为1
for (int i = n-1; i >= 0; i--) {
answer[i] *= suffix; // 前缀乘积 × 后缀乘积 = 最终答案
suffix *= nums[i]; // 更新后缀乘积,包含当前元素,供左边元素使用
}
return answer;
}
};
// 测试主函数(可直接运行,和前面格式一致)
int main() {
// 测试示例1
cout << "测试题目示例1:" << endl;
vector<int> nums1 = {1,2,3,4};
Solution sol;
vector<int> res1 = sol.productExceptSelf(nums1);
cout << "输入数组:";
for (int num : nums1) cout << num << " ";
cout << endl << "输出结果:";
for (int num : res1) cout << num << " ";
cout << endl << "------------------------" << endl;
// 测试示例2
cout << "测试题目示例2:" << endl;
vector<int> nums2 = {-1,1,0,-3,3};
vector<int> res2 = sol.productExceptSelf(nums2);
cout << "输入数组:";
for (int num : nums2) cout << num << " ";
cout << endl << "输出结果:";
for (int num : res2) cout << num << " ";
cout << endl << "------------------------" << endl;
// 自定义输入测试
cout << "自定义输入测试:" << endl;
int n;
cout << "请输入数组长度:";
cin >> n;
vector<int> nums3(n);
cout << "请输入" << n << "个整数(空格分隔):";
for (int i = 0; i < n; i++) {
cin >> nums3[i];
}
vector<int> res3 = sol.productExceptSelf(nums3);
cout << "输入数组:";
for (int num : nums3) cout << num << " ";
cout << endl << "输出结果:";
for (int num : res3) cout << num << " ";
cout << endl;
return 0;
}
5.4 代码运行过程(必看,以示例1为例)
输入 nums = [1,2,3,4],n=4,answer初始为 [1,1,1,1],suffix初始为1
第一步:正序遍历,计算前缀乘积(answer当left用)
-
i=1:answer[1] = answer[0] × nums[0] = 1×1 = 1 → answer = [1,1,1,1]
-
i=2:answer[2] = answer[1] × nums[1] = 1×2 = 2 → answer = [1,1,2,1]
-
i=3:answer[3] = answer[2] × nums[2] = 2×3 = 6 → answer = [1,1,2,6](此时answer就是解法二的left数组)
第二步:倒序遍历,动态计算后缀乘积(suffix),更新answer
- i=3(最右边):
answer[3] = answer[3] × suffix = 6×1 = 6(前缀乘积6 × 后缀乘积1 = 最终答案6)
suffix = suffix × nums[3] = 1×4 = 4(更新后缀乘积,供i=2使用)
此时answer = [1,1,2,6],suffix=4
- i=2:
answer[2] = answer[2] × suffix = 2×4 = 8(前缀乘积2 × 后缀乘积4 = 最终答案8)
suffix = suffix × nums[2] = 4×3 = 12(更新后缀乘积,供i=1使用)
此时answer = [1,1,8,6],suffix=12
- i=1:
answer[1] = answer[1] × suffix = 1×12 = 12(前缀乘积1 × 后缀乘积12 = 最终答案12)
suffix = suffix × nums[1] = 12×2 = 24(更新后缀乘积,供i=0使用)
此时answer = [1,12,8,6],suffix=24
- i=0(最左边):
answer[0] = answer[0] × suffix = 1×24 = 24(前缀乘积1 × 后缀乘积24 = 最终答案24)
suffix = suffix × nums[0] = 24×1 = 24(更新后缀乘积,无后续使用)
最终answer = [24,12,8,6],和示例1一致,且只用到1个额外变量,空间最优。
5.5 复杂度分析(最优解核心)
-
时间复杂度:O(n) → 只遍历2次数组(正序1次、倒序1次),总操作次数2n,属于O(n),比解法二更高效;
-
空间复杂度:O(1) → 只用到1个额外变量(suffix),没有开任何和原数组长度相同的额外数组,完美满足题目进阶要求,是面试首选解法。
六、三种解法对比
为了方便对比选择,整理了三种解法的核心信息,不用记复杂内容,看表格就能懂:
| 解法 | 核心思路 | 时间复杂度 | 空间复杂度 | 是否能提交 | 适用场景 |
|---|---|---|---|---|---|
| 暴力解法 | 双重循环,逐个相乘排除当前元素 | O(n²) | O(1) | 不能(超时) | 理解思路,知道坑点 |
| 左右乘积数组 | 前缀乘积数组+后缀乘积数组,相乘得答案 | O(n) | O(n) | 能 | 入门,易理解,笔试保底 |
| 原地优化(最优解) | 答案数组存前缀乘积,变量存动态后缀乘积 | O(n) | O(1) | 能 | 面试首选,空间最优,体现算法能力 |
七、必看总结
-
本题核心技巧:把"除自身外的乘积"拆成"前缀乘积 × 后缀乘积",这是避开除法、实现O(n)时间的关键;
-
三个解法的递进关系:暴力解法(直观但超时)→ 左右乘积数组(解决超时,空间一般)→ 原地优化(空间最优,面试重点);
-
学习顺序:先看懂暴力解法的坑点,再掌握左右乘积数组(易理解),最后吃透原地优化(核心考点);
-
代码注意事项:
-
Python:注意List类型导入,测试主函数的输入处理,循环的范围(正序从1开始,倒序从n-2开始);
-
C++:注意vector的初始化和遍历方式,输入输出的格式,避免遗漏return 0;
- 测试技巧:所有代码都带测试主函数,可直接复制运行,先测试题目示例,再自定义输入验证,确保代码正确。
只要掌握"前缀乘积+后缀乘积"的核心思路,无论题目怎么变(比如数组长度变化、有负数/0),都能轻松解决!