https://leetcode.cn/problems/product-of-array-except-self/description/
一、题目分析
给你一个整数数组 nums
,返回 数组 answer
,其中 answer[i]
等于 nums
中除 nums[i]
之外其余各元素的乘积 。
题目数据 保证 数组 nums
之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。
请 不要使用除法,且在 O(n)
时间复杂度内完成此题。
二、示例分析
示例 1:
输入: nums =[1,2,3,4] 输出:[24,12,8,6]
示例 2:
输入: nums = [-1,1,0,-3,3] 输出: [0,0,9,0,0]
结合题目以及示例,我们不难看出今天这道题目的需要完成的操作就是求出除自身以外的乘积,并存放到一个数组中,最后返回数组。
三、解题思路&代码实现
在拿到任何一道题目的时候,我们首先要做的就是认真审题,读懂题目要求以后,在心里或者草稿纸上写出模拟过程的伪代码,之后再做进一步的优化。今天这道题目也一样,我们什么算法都先不要考虑,就用最暴力的方法来做。
1、暴力解法(复杂度为O(n^2))
1、核心思路:对于每一个nums[i],遍历整个数组,跳过nums[i]并计算其余元素的乘积
2、关键步骤:
1、初始化ans数组,该数组的长度于nums数组相同
2、双重循环计算乘积:
外层循环:遍历每个nums[i],准备计算ans[i]。
内层循环:遍历nums所有元素,遇到 i == j时跳出此次循环(排除自身),其余元素累乘到sum。
3、存储结果:
将计算好的sum存入到ans[i]中。
vector<int> productExceptSelf(vector<int>& nums) {vector<int> ans(nums.size()); // 定义结果数组,长度与 nums 相同// 遍历计算每个位置 ans[i] 的值for (int i = 0; i < nums.size(); i++) {long long sum = 1; // 初始化乘积为 1(因为 1 不影响乘法结果)// 计算 nums 中除 nums[i] 外所有元素的乘积for (int j = 0; j < nums.size(); j++) {if (j == i) continue; // 跳过当前元素 nums[i],不参与乘积计算sum *= nums[j]; // 累乘其他元素}ans[i] = sum; // 将计算结果存入 ans[i]}return ans; // 返回最终结果
}
但是本题暴力解法是并不能通过所有的测试用例,题目的数据范围给到了1e5,如果是双重循环的话就是1e10。(PS:C/C++ 1秒能跑的数据大概是在1e7~1e8左右)。
那么现在就要想办法优化把这段程序优化至O(n);
2、分类讨论(数学分类法)
数学分类法的核心思路:
首先要统计出数组中是否有零,如果有的话零有多少个,如果有且仅有一个的话,那么ans数组中的元素就应该是除nums数组元素中为0的那一个元素应该为其余元素的乘积,其他元素则全部为零。
如果nums数组中0的个数>1,那么ans数组中的所有元素都应为0;
除以上两种情况外 也就是nums数组中没有零。
- 单零情况:若数组中仅含一个零,那么结果数组中,零所在位置的元素为其余非零元素的乘积,其余位置元素均为零。例如,对于数组
[1, 0, 2, 3]
,结果数组为[0, 6, 0, 0]
,因为1*2*3=6
,零位置填充该值,其余位置补零。 - 多零情况:当数组中零的数量大于 1 时,无论如何计算,乘积必然为零,因此结果数组的所有元素均为零。
- 无零情况:若数组中不存在零,结果数组的每个元素等于数组所有元素的乘积除以对应位置的元素。例如,对于数组
[1, 2, 3, 4]
,结果数组为[24, 12, 8, 6]
,因为1*2*3*4=24
,分别除以1
、2
、3
、4
得到对应位置结果。
class Solution {
public:vector<int> productExceptSelf(vector<int>& nums) {// 初始化结果数组,大小与输入数组相同vector<int> ans(nums.size());// sum: 所有元素的乘积(初始为1)// cnt_0: 统计数组中0的个数int sum = 1, cnt_0 = 0;// 第一次遍历:计算所有元素的乘积,并统计0的个数for (auto i : nums) {sum *= i; // 累乘所有元素if (i == 0) // 遇到0时计数cnt_0++;}// 情况1:数组中恰好有1个0if (cnt_0 == 1) {int sum = 1; // 重新计算非0元素的乘积(避免之前sum=0的影响)// 计算所有非0元素的乘积for (int i = 0; i < nums.size(); i++) {if (nums[i] != 0)sum *= nums[i];}// 填充结果数组:// - 非0位置的结果为0(因为总乘积含0)// - 0位置的结果为非0元素的乘积for (int i = 0; i < nums.size(); i++) {if (nums[i] != 0)ans[i] = 0;elseans[i] = sum;}return ans;}// 情况2:数组中有超过1个0// 所有位置的结果都是0(因为任何位置都至少包含一个0)if (cnt_0 > 1) {return ans; // ans已初始化为全0}// 情况3:数组中没有0// 直接计算:ans[i] = 总乘积 / nums[i]for (int i = 0; i < nums.size(); i++) {ans[i] = sum / nums[i];}return ans;}
};
以上代码是优化了时间复杂度,但是题目中也是有明确要求不能使用除法的,所以尽管我们这段代码的时间复杂度已经优化至O(n)。但还是需要改进。
3、前缀和&后缀和思想(正解√)
核心思想:
此方法的核心思想就是空间换时间,利用前缀和的思想的对ans数组进行预处理,这样的好处就是每次取一个结果所需的时间复杂度为常数级,也就是O(1)。
其次通过将目标乘积拆分为两部分(左部分、右部分)。第一次看到这段代码时候,应该会想为什么能把nums[i]排除呢?
-
左边乘积(
ans[i]
)不包含nums[i]
(因为只乘到nums[i-1]
)。 -
右边乘积(
right
)不包含nums[i]
(因为先乘right
,再更新right
)。 -
最终结果:
ans[i] = 左边乘积 × 右边乘积
,自然排除了nums[i]
。
class Solution {
public:vector<int> productExceptSelf(vector<int>& nums) {// 初始化结果数组,所有元素初始值为1(乘法单位元)vector<int> ans(nums.size(), 1);// 第一次遍历:计算每个元素左边所有元素的乘积(前缀积)// ans[i] 表示 nums[0] × nums[1] × ... × nums[i-1]for (int i = 1; i < nums.size(); i++) {ans[i] = ans[i - 1] * nums[i - 1]; // 递推计算前缀积}// 初始化右边乘积为1(最右边元素的右边没有元素)int right = 1;// 第二次遍历(从右往左):计算右边乘积并合并到结果中// 此时 ans[i] = 左边乘积 × 右边乘积for (int i = nums.size() - 1; i >= 0; i--) {ans[i] *= right; // 将当前元素的右边乘积乘到结果上right *= nums[i]; // 更新右边乘积(包含当前元素)}return ans;}
};
至此,上述代码均满足题目的所有要求!!!完结撒花🌸🌸🌸
四、题目总结
本题要求在不使用除法且时间复杂度为 O(n) 的条件下,计算数组中除自身元素之外其余各元素的乘积。解题过程需结合题目要求与数据限制,通过逐步优化算法来实现目标。
在暴力解法中,采用双重循环遍历数组,对每个元素计算其余元素的乘积,虽然逻辑直观,但时间复杂度高达 O(n^2),无法满足题目数据范围的要求。
数学分类法通过统计数组中零的数量进行分类讨论,优化了时间复杂度至 O(n)。当数组中有一个零,结果数组中零位置为其余非零元素乘积,其余位置为零;若有多个零,结果数组全为零;若无零,则用所有元素乘积除以对应位置元素得到结果。然而,该方法使用了除法运算,不符合题目要求。
最终的前缀和与后缀和思想是本题正解。利用空间换时间,通过两次遍历分别计算前缀积和后缀积,将目标乘积拆分为左右两部分。先从前向后计算前缀积存入结果数组,再从后向前更新后缀积并与前缀积合并,既避免了除法运算,又满足了 O(n) 的时间复杂度要求,高效且准确地解决了问题 。这种解题过程体现了在算法设计中平衡时间复杂度、空间复杂度与题目限制条件的重要性。
算法之美在于其严谨的逻辑与精巧的设计。希望读者能通过本题,掌握空间换时间的核心思想,在日后的开发中灵活运用这种预处理技巧。谢谢大家!!!荆轲刺秦!