一、题目核心拆解(必看)
先抛开专业术语,用大白话把题目说透,确保你完全理解需求:
给定一个整数数组(比如 [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]
题目强制要求(重点!):
1. 不能用除法(比如先算所有数的总乘积,再除以每个数——不行!因为数组里可能有0,除以0会报错,而且题目明确禁止);
2. 时间复杂度必须是 O(n)(简单说:只能遍历数组1~3次,不能嵌套遍历,否则会超时);
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 数组的遍历(核心操作)
遍历就是“逐个访问数组里的每个数字”,常见两种方式,和解法密切相关:
1. 正序遍历:从索引0开始,依次访问到最后一个索引(n-1,n是数组长度);
例子:nums = [1,2,3,4],正序遍历顺序是:nums[0](1)→ nums[1](2)→ nums[2](3)→ nums[3](4)。
2. 倒序遍历:从最后一个索引(n-1)开始,依次访问到索引0;
例子:nums = [1,2,3,4],倒序遍历顺序是:nums[3](4)→ nums[2](3)→ nums[1](2)→ nums[0](1)。
2.3 前缀乘积 & 后缀乘积(解法核心技巧)
这两个是本题的“灵魂”,一定要吃透,用具体例子讲透:
1. 前缀乘积(左乘积):对于数组中某个位置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]。
2. 后缀乘积(右乘积):对于数组中某个位置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 时间复杂度 & 空间复杂度(题目强制要求)
不用记复杂公式,用大白话+例子讲懂:
1. 时间复杂度:衡量“算法执行快慢”,用 O(...) 表示,核心看“遍历次数”;
- O(n):只遍历数组1~3次(n是数组长度),效率极高;比如遍历一次数组,执行n次操作,就是O(n);
- O(n²):嵌套遍历(比如外层遍历n次,内层也遍历n次),执行n×n次操作,效率极低;本题n最大是100000,O(n²)会执行10000000000次操作,电脑处理不了,会“超时”(无法通过测试)。
2. 空间复杂度:衡量“算法占用内存多少”,用 O(...) 表示,核心看“是否开额外数组”;
- O(1):只用到几个变量(比如int a=1,没有开和原数组长度一样的额外数组);
- O(n):开了一个或多个“和原数组长度相同”的额外数组(比如前缀乘积数组left,长度和nums一样,就是O(n));
注意:题目说“输出数组不算额外空间”,所以存储答案的数组(answer),不算在空间复杂度里。
2.5 为什么不能用除法?(补充说明)
很多会想:“先算所有数的总乘积,再除以每个数,不就得到答案了?”——不行!原因有2个:
1. 数组中可能有0:除以0是数学错误,程序会报错;
2. 题目明确禁止用除法(考察的是“前缀/后缀乘积”的思路,不是除法技巧)。
三、解法一:暴力解法
先从最直观的思路入手,虽然会超时,但能帮你理解“为什么需要优化”,也能轻松看懂。
3.1 核心思路(大白话)
对数组中每个位置i,都重新遍历一次数组,把“除了nums[i]之外的所有数”乘起来,存到answer[i]里。
简单说:双重循环,外层循环找“当前要计算的位置i”,内层循环找“除了i之外的所有数”,相乘即可。
3.2 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++代码(逐句注释,能看懂每一步)
#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]
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]
2. 当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]
3. 当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]
4. 当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 核心思路(大白话)
1. 开一个“前缀乘积数组left”,存储每个位置i的“左边所有数的乘积”;
2. 开一个“后缀乘积数组right”,存储每个位置i的“右边所有数的乘积”;
3. 答案数组answer[i] = left[i] × right[i];
这样只需要遍历3次数组(left一次、right一次、answer一次),时间复杂度O(n),不会超时。
4.2 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++代码(逐句注释,含测试主函数)
#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 复杂度分析(能懂)
1. 时间复杂度:O(n) → 总共遍历3次数组(left一次、right一次、answer一次),每次遍历n个元素,总操作次数是3n,属于O(n)(常数倍不影响复杂度);
2. 空间复杂度:O(n) → 额外开了两个长度为n的数组(left和right),符合题目基本要求,但不符合进阶的O(1)空间要求,所以我们需要进一步优化。
五、解法三:原地优化(进阶最优,O(1)额外空间)
这是本题的“最优解”,在解法二的基础上优化空间,去掉额外的left和right数组,只用答案数组和一个变量,完美满足题目进阶要求(O(1)额外空间+O(n)时间),也是面试中最常考的写法。
5.1 核心思路(大白话,结合解法二优化)
解法二的缺点是“开了两个额外数组”,我们可以利用“答案数组不算额外空间”的规则,做两个优化:
1. 不用单独开left数组:把“前缀乘积”直接存在answer数组里(answer先当left用);
2. 不用单独开right数组:用一个变量(suffix)动态计算“后缀乘积”,倒序遍历的时候,直接用这个变量和answer里的前缀乘积相乘,得到最终答案。
简单说:answer先存前缀乘积,再用一个变量存动态后缀乘积,一次倒序遍历完成最终计算,省掉两个额外数组。
5.2 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++代码(逐句注释,含测试主函数)
#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 复杂度分析(最优解核心)
1. 时间复杂度:O(n) → 只遍历2次数组(正序1次、倒序1次),总操作次数2n,属于O(n),比解法二更高效;
2. 空间复杂度:O(1) → 只用到1个额外变量(suffix),没有开任何和原数组长度相同的额外数组,完美满足题目进阶要求,是面试首选解法。
六、三种解法对比
为了方便对比选择,整理了三种解法的核心信息,不用记复杂内容,看表格就能懂:
解法 | 核心思路 | 时间复杂度 | 空间复杂度 | 是否能提交 | 适用场景 |
|---|---|---|---|---|---|
暴力解法 | 双重循环,逐个相乘排除当前元素 | O(n²) | O(1) | 不能(超时) | 理解思路,知道坑点 |
左右乘积数组 | 前缀乘积数组+后缀乘积数组,相乘得答案 | O(n) | O(n) | 能 | 入门,易理解,笔试保底 |
原地优化(最优解) | 答案数组存前缀乘积,变量存动态后缀乘积 | O(n) | O(1) | 能 | 面试首选,空间最优,体现算法能力 |
七、必看总结
1. 本题核心技巧:把“除自身外的乘积”拆成“前缀乘积 × 后缀乘积”,这是避开除法、实现O(n)时间的关键;
2. 三个解法的递进关系:暴力解法(直观但超时)→ 左右乘积数组(解决超时,空间一般)→ 原地优化(空间最优,面试重点);
3. 学习顺序:先看懂暴力解法的坑点,再掌握左右乘积数组(易理解),最后吃透原地优化(核心考点);
4. 代码注意事项:
- Python:注意List类型导入,测试主函数的输入处理,循环的范围(正序从1开始,倒序从n-2开始);
- C++:注意vector的初始化和遍历方式,输入输出的格式,避免遗漏return 0;
5. 测试技巧:所有代码都带测试主函数,可直接复制运行,先测试题目示例,再自定义输入验证,确保代码正确。
只要掌握“前缀乘积+后缀乘积”的核心思路,无论题目怎么变(比如数组长度变化、有负数/0),都能轻松解决!