力扣解题-238. 除了自身以外数组的乘积

0 阅读7分钟

力扣解题-238. 除了自身以外数组的乘积

给你一个整数数组 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]

提示:

2 <= nums.length <= 105

-30 <= nums[i] <= 30

输入 保证 数组 answer[i] 在 32 位 整数范围内

进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)

Related Topics

数组、前缀和


第一次解答

解题思路

核心方法:左右前缀乘积数组法,通过两个辅助数组分别存储“前缀乘积”和“后缀乘积”,再通过两者的组合计算每个位置的结果,逻辑直观且满足O(n)时间复杂度要求,但额外使用了两个数组导致空间复杂度为O(n)。

核心逻辑拆解

“除自身外所有元素的乘积”可拆解为“当前元素左边所有元素的乘积 × 右边所有元素的乘积”:

  1. 前缀乘积数组leftleft[i]表示nums[0]nums[i]的累积乘积(即i位置及左侧所有元素的乘积);
  2. 后缀乘积数组rightright[i]表示nums[i]nums[length-1]的累积乘积(即i位置及右侧所有元素的乘积);
  3. 结果计算
    • i=0(第一个元素):无左侧元素,结果=右侧所有元素的乘积=right[1]
    • i=length-1(最后一个元素):无右侧元素,结果=左侧所有元素的乘积=left[length-2]
    • 其他位置:结果=左侧所有元素乘积(left[i-1]) × 右侧所有元素乘积(right[i+1])。
具体步骤(以nums=[1,2,3,4]为例)
  1. 计算left数组:left[0]=1,left[1]=1×2=2,left[2]=2×3=6,left[3]=6×4=24;
  2. 计算right数组:right[3]=4,right[2]=3×4=12,right[1]=2×12=24,right[0]=1×24=24;
  3. 计算结果:
    • result[0] = right[1] = 24;
    • result[1] = left[0] × right[2] = 1×12=12;
    • result[2] = left[1] × right[3] = 2×4=8;
    • result[3] = left[2] = 6;
    • 最终结果:[24,12,8,6]。
性能说明
  • 时间复杂度:O(n)(三次线性遍历:计算left、计算right、计算结果),耗时3ms仅击败10.84%用户(主要因额外数组的内存操作开销);
  • 空间复杂度:O(n)(left和right数组各占用n个空间),内存消耗63.7MB击败81.69%用户;
  • 优势:逻辑清晰,新手易理解,完全规避了除法操作(符合题目要求);
  • 可优化点:额外使用两个数组,未利用“输出数组不计入额外空间”的规则,空间复杂度未达进阶要求。
    public int[] productExceptSelf(int[] nums) {
        int length=nums.length;
        int [] left =new int[length];
        int [] right=new int[length];
        int [] result=new int[length];
        left[0]=nums[0];
        for(int i=1;i<length;i++){
            left[i]=nums[i]*left[i-1];
        }
        right[length-1]=nums[length-1];
        for(int i=length-2;i>=0;i--){
            right[i]=nums[i]*right[i+1];
        }
        for(int i=0;i<length;i++){
            if(i==0){
                result[i]=right[i+1];
            }else if(i==length-1){
                result[i]=left[length-2];
            }else {
                result[i]=left[i-1]*right[i+1];
            }
        }
        return result;
    }

示例解答

解题思路

解法1:空间优化版(O(1)额外空间,最优解)

核心方法:输出数组复用 + 单次遍历填充,利用输出数组先存储前缀乘积,再通过反向遍历用变量记录后缀乘积,最终组合得到结果,仅使用常数级额外空间(满足进阶要求)。

核心优化逻辑
  1. 第一步:输出数组存储前缀乘积
    • 初始化result[0] = 1(第一个元素左侧无元素,前缀乘积为1);
    • 遍历数组,result[i] = result[i-1] × nums[i-1]result[i]表示i位置左侧所有元素的乘积);
  2. 第二步:反向遍历计算后缀乘积并组合结果
    • 初始化变量suffix = 1(最后一个元素右侧无元素,后缀乘积为1);
    • 从后往前遍历,result[i] = result[i] × suffix(前缀×后缀);
    • 更新suffix = suffix × nums[i](将当前元素加入后缀乘积,供前一个位置使用)。
代码实现
public int[] productExceptSelf(int[] nums) {
    int n = nums.length;
    int[] result = new int[n];
    
    // 第一步:计算前缀乘积(存储在result中)
    result[0] = 1;
    for (int i = 1; i < n; i++) {
        result[i] = result[i-1] * nums[i-1];
    }
    
    // 第二步:反向计算后缀乘积并组合结果
    int suffix = 1;
    for (int i = n-1; i >= 0; i--) {
        result[i] = result[i] * suffix;
        suffix *= nums[i];
    }
    
    return result;
}
性能优势
  • 时间复杂度:O(n)(两次线性遍历,比原解法少一次遍历),耗时可降至1ms左右(击败99%+用户);
  • 额外空间复杂度:O(1)(仅使用suffix一个变量,输出数组不计入额外空间);
  • 核心优化点:
    1. 复用输出数组存储前缀乘积,消除left数组的空间开销;
    2. 用变量替代right数组,反向遍历过程中动态计算后缀乘积;
    3. 减少数组内存分配和访问的开销,提升执行效率。
具体步骤(以nums=[1,2,3,4]为例)
  1. 前缀乘积填充result:
    • result[0] = 1;
    • result[1] = result[0]×nums[0] = 1×1=1;
    • result[2] = result[1]×nums[1] = 1×2=2;
    • result[3] = result[2]×nums[2] = 2×3=6;
    • 此时result = [1,1,2,6];
  2. 反向遍历计算后缀:
    • i=3:result[3] = 6×1=6,suffix=1×4=4;
    • i=2:result[2] = 2×4=8,suffix=4×3=12;
    • i=1:result[1] = 1×12=12,suffix=12×2=24;
    • i=0:result[0] = 1×24=24,suffix=24×1=24;
    • 最终result = [24,12,8,6]。
解法2:分治优化版(兼容大数场景)

核心方法:拆分正负/零的乘积逻辑,针对数组包含0或负数的场景,通过统计0的个数、负数的个数,结合总乘积计算结果(需注意:仅当数组无0时可用此方法,且需确保总乘积不溢出)。

代码实现(补充思路,非最优但拓展思维)
public int[] productExceptSelf(int[] nums) {
    int n = nums.length;
    int[] result = new int[n];
    int totalProduct = 1;
    int zeroCount = 0;
    
    // 计算总乘积和0的个数
    for (int num : nums) {
        if (num == 0) {
            zeroCount++;
            continue;
        }
        totalProduct *= num;
    }
    
    // 根据0的个数计算结果
    for (int i = 0; i < n; i++) {
        if (zeroCount > 1) {
            // 多个0,所有位置结果都是0
            result[i] = 0;
        } else if (zeroCount == 1) {
            // 一个0,只有0的位置结果为总乘积,其余为0
            result[i] = nums[i] == 0 ? totalProduct : 0;
        } else {
            // 无0,结果=总乘积/当前元素(题目禁止除法,仅作拓展)
            result[i] = totalProduct / nums[i];
        }
    }
    return result;
}
适用场景说明
  • 该方法仅作思路拓展,题目明确禁止使用除法,因此不推荐作为正式解法;
  • 优势:在无0且无溢出的场景下,仅需两次遍历,计算更简洁;
  • 局限性:无法处理数组包含0的场景(除法无意义),且大数相乘易溢出,不符合题目要求。

总结

  1. 左右前缀数组法(第一次解答):逻辑直观,O(n)时间+O(n)空间,适合新手理解核心原理;
  2. 空间优化版(最优解):O(n)时间+O(1)额外空间,复用输出数组+动态后缀变量,满足进阶要求,工程首选;
  3. 分治优化版:仅作思路拓展,因使用除法不符合题目要求,实际不可用;
  4. 关键技巧:
    • 核心思想:将“除自身外乘积”拆解为“前缀×后缀”,避免暴力遍历;
    • 空间优化:利用“输出数组不计入额外空间”的规则,复用数组存储中间结果;
    • 边界处理:注意首尾元素的前缀/后缀为空的情况(乘积为1)。