【Hot100|15-LeetCode 238. 除自身以外数组的乘积】

LeetCode 238. 除自身以外数组的乘积 - 完整解法详解

一、问题理解

问题描述

给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。要求不使用除法 ,在 O(n) 时间复杂度内完成,并且不能使用额外的数组空间(输出数组不被视为额外空间)。

示例

text

复制代码
输入: nums = [1,2,3,4]
输出: [24,12,8,6]
解释: 
对于索引0,乘积 = 2×3×4 = 24
对于索引1,乘积 = 1×3×4 = 12
对于索引2,乘积 = 1×2×4 = 8
对于索引3,乘积 = 1×2×3 = 6

输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]
解释: 注意处理0的情况

要求

  • 不使用除法

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)(输出数组不计入空间复杂度)

二、核心思路:前缀积与后缀积

基本思想

对于数组中的每个位置 i,我们需要计算除 nums[i] 外所有元素的乘积。这可以分解为两个部分:

  1. 前缀积nums[0] × nums[1] × ... × nums[i-1]

  2. 后缀积nums[i+1] × nums[i+2] × ... × nums[n-1]

那么 answer[i] = 前缀积 × 后缀积

优化方法

为了避免重复计算,我们可以:

  1. 从左到右计算前缀积并存储

  2. 从右到左计算后缀积并与前缀积相乘

三、代码逐行解析

方法一:使用两个数组(空间复杂度 O(n))

Java 解法

java

复制代码
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        
        // 1. 计算前缀积
        // pre[i] 表示 nums[0] × nums[1] × ... × nums[i-1] 的乘积
        int[] pre = new int[n];
        pre[0] = 1;
        for (int i = 1; i < n; i++) {
            pre[i] = pre[i-1] * nums[i-1];
        }
        
        // 2. 计算后缀积
        // suf[i] 表示 nums[i+1] × nums[i+2] × ... × nums[n-1] 的乘积
        int[] suf = new int[n];
        suf[n-1] = 1;
        for (int i = n-2; i >= 0; i--) {
            suf[i] = suf[i+1] * nums[i+1];
        }
        
        // 3. 计算最终结果:前缀积 × 后缀积
        int[] answer = new int[n];
        for (int i = 0; i < n; i++) {
            answer[i] = pre[i] * suf[i];
        }
        
        return answer;
    }
}
Python 解法

python

复制代码
from typing import List

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        
        # 1. 计算前缀积
        # pre[i] 表示 nums[0] × nums[1] × ... × nums[i-1] 的乘积
        pre = [1] * n
        for i in range(1, n):
            pre[i] = pre[i-1] * nums[i-1]
        
        # 2. 计算后缀积
        # suf[i] 表示 nums[i+1] × nums[i+2] × ... × nums[n-1] 的乘积
        suf = [1] * n
        for i in range(n-2, -1, -1):
            suf[i] = suf[i+1] * nums[i+1]
        
        # 3. 计算最终结果:前缀积 × 后缀积
        answer = [0] * n
        for i in range(n):
            answer[i] = pre[i] * suf[i]
        
        return answer

方法二:优化空间复杂度(O(1) 额外空间)

Java 解法

java

复制代码
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] answer = new int[n];
        
        // 1. 先计算前缀积,直接存储在 answer 中
        // 此时 answer[i] = nums[0] × nums[1] × ... × nums[i-1]
        answer[0] = 1;
        for (int i = 1; i < n; i++) {
            answer[i] = answer[i-1] * nums[i-1];
        }
        
        // 2. 计算后缀积,并乘到 answer 中
        // 用一个变量 suffix 记录当前的后缀积
        int suffix = 1;
        for (int i = n-1; i >= 0; i--) {
            // 将后缀积乘到当前结果上
            answer[i] *= suffix;
            // 更新后缀积:包含当前元素,为下一轮计算做准备
            suffix *= nums[i];
        }
        
        return answer;
    }
}
Python 解法

python

复制代码
from typing import List

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        answer = [1] * n
        
        # 1. 先计算前缀积,直接存储在 answer 中
        # 此时 answer[i] = nums[0] × nums[1] × ... × nums[i-1]
        for i in range(1, n):
            answer[i] = answer[i-1] * nums[i-1]
        
        # 2. 计算后缀积,并乘到 answer 中
        # 用一个变量 suffix 记录当前的后缀积
        suffix = 1
        for i in range(n-1, -1, -1):
            # 将后缀积乘到当前结果上
            answer[i] *= suffix
            # 更新后缀积:包含当前元素,为下一轮计算做准备
            suffix *= nums[i]
        
        return answer

四、Java 与 Python 语法对比

1. 数组/列表操作

操作 Java Python
创建数组/列表 int[] arr = new int[n]; arr = [0] * n
数组/列表长度 arr.length len(arr)
遍历数组 for (int i=0; i<n; i++) for i in range(n):
逆序遍历 for (int i=n-1; i>=0; i--) for i in range(n-1, -1, -1):

2. 变量初始化

操作 Java Python
初始化数组 int[] arr = new int[n]; arr = [0] * n
初始化列表 List<Integer> list = new ArrayList<>(); list = []

3. 数学运算

操作 Java Python
乘法 a * b a * b
赋值 a = b a = b
自乘赋值 a *= b a *= b

五、实例演示

nums = [1, 2, 3, 4] 为例,演示两种方法的过程:

方法一(两个数组)

Java/Python 步骤1:计算前缀积

text

复制代码
pre[0] = 1
pre[1] = pre[0] × nums[0] = 1 × 1 = 1
pre[2] = pre[1] × nums[1] = 1 × 2 = 2
pre[3] = pre[2] × nums[2] = 2 × 3 = 6
pre = [1, 1, 2, 6]

Java/Python 步骤2:计算后缀积

text

复制代码
suf[3] = 1
suf[2] = suf[3] × nums[3] = 1 × 4 = 4
suf[1] = suf[2] × nums[2] = 4 × 3 = 12
suf[0] = suf[1] × nums[1] = 12 × 2 = 24
suf = [24, 12, 4, 1]

Java/Python 步骤3:计算最终结果

text

复制代码
answer[0] = pre[0] × suf[0] = 1 × 24 = 24
answer[1] = pre[1] × suf[1] = 1 × 12 = 12
answer[2] = pre[2] × suf[2] = 2 × 4 = 8
answer[3] = pre[3] × suf[3] = 6 × 1 = 6
answer = [24, 12, 8, 6]

方法二(优化空间)

Java/Python 步骤1:计算前缀积到 answer 中

text

复制代码
answer[0] = 1
answer[1] = answer[0] × nums[0] = 1 × 1 = 1
answer[2] = answer[1] × nums[1] = 1 × 2 = 2
answer[3] = answer[2] × nums[2] = 2 × 3 = 6
answer = [1, 1, 2, 6]  # 此时 answer[i] = 前缀积

Java/Python 步骤2:计算后缀积并乘到 answer 中

text

复制代码
初始化 suffix = 1

i=3: answer[3] *= suffix → 6 × 1 = 6
     suffix *= nums[3] → 1 × 4 = 4

i=2: answer[2] *= suffix → 2 × 4 = 8
     suffix *= nums[2] → 4 × 3 = 12

i=1: answer[1] *= suffix → 1 × 12 = 12
     suffix *= nums[1] → 12 × 2 = 24

i=0: answer[0] *= suffix → 1 × 24 = 24
     suffix *= nums[0] → 24 × 1 = 24

最终 answer = [24, 12, 8, 6]

六、关键细节解析

1. 为什么前缀积数组的初始值都是 1?

  • 对于第一个元素,它没有前缀元素,所以前缀积应该是 1(乘法单位元)

  • 对于其他元素,前缀积初始为 1,然后逐步乘以前面的元素

2. 如何处理数组边界?

  • 前缀积:从第二个元素开始计算,避免数组越界

  • 后缀积:从倒数第二个元素开始向前计算,避免数组越界

3. 为什么优化版本的空间复杂度是 O(1)?

  • 只使用了输出数组 answer 和一个变量 suffix

  • 输出数组不计入空间复杂度分析

  • 因此额外空间是常数级别

4. 如何处理包含 0 的数组?

算法天然支持 0 的情况:

text

复制代码
nums = [1, 0, 3, 4]
前缀积: [1, 1, 0, 0]
后缀积: [0, 12, 4, 1]
结果: [0, 12, 0, 0]

七、复杂度分析

方法一(两个数组)

  • 时间复杂度:O(n),需要三次遍历数组

  • 空间复杂度:O(n),使用了两个额外数组

方法二(优化空间)

  • 时间复杂度:O(n),需要两次遍历数组

  • 空间复杂度:O(1),只使用了常数额外空间(输出数组不计入)

八、其他解法

解法一:使用除法(不符合题目要求)

Java 解法

java

复制代码
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] answer = new int[n];
        
        // 计算所有元素的乘积
        int totalProduct = 1;
        int zeroCount = 0;
        int zeroIndex = -1;
        
        for (int i = 0; i < n; i++) {
            if (nums[i] == 0) {
                zeroCount++;
                zeroIndex = i;
                if (zeroCount > 1) {
                    // 如果有多个0,所有结果都是0
                    return new int[n]; // 默认都是0
                }
            } else {
                totalProduct *= nums[i];
            }
        }
        
        // 计算答案
        if (zeroCount == 1) {
            // 只有一个0,那么只有该位置的结果不为0
            answer[zeroIndex] = totalProduct;
        } else {
            // 没有0,正常计算
            for (int i = 0; i < n; i++) {
                answer[i] = totalProduct / nums[i];
            }
        }
        
        return answer;
    }
}
Python 解法

python

复制代码
from typing import List

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        answer = [0] * n
        
        # 计算所有元素的乘积
        total_product = 1
        zero_count = 0
        zero_index = -1
        
        for i, num in enumerate(nums):
            if num == 0:
                zero_count += 1
                zero_index = i
                if zero_count > 1:
                    # 如果有多个0,所有结果都是0
                    return answer
            else:
                total_product *= num
        
        # 计算答案
        if zero_count == 1:
            # 只有一个0,那么只有该位置的结果不为0
            answer[zero_index] = total_product
        else:
            # 没有0,正常计算
            for i in range(n):
                answer[i] = total_product // nums[i]
        
        return answer

九、常见问题与解答

Q1: 如果数组中有 0 怎么办?

A1: 算法天然支持 0 的情况。当遇到 0 时:

  • 如果只有一个 0,那么除了该位置外,其他位置的结果都是 0

  • 如果有多个 0,那么所有位置的结果都是 0

Q2: 为什么不能使用除法?

A2: 题目明确要求不使用除法。此外,如果使用除法:

  • 需要处理 0 的情况

  • 如果是整数除法可能产生精度问题

  • 不符合题目考察的目的(考察前缀积和后缀积的思想)

Q3: 如果数组很大怎么办?

A3: 算法的时间复杂度是 O(n),空间复杂度是 O(1)(优化版本),可以处理非常大的数组。

Q4: 这个方法适用于负数吗?

A4: 是的,算法适用于包含负数的数组,因为乘法对负数同样有效。

Q5: Java 和 Python 版本有什么区别?

A5: 主要区别在于语法:

  • Java 需要显式声明数组类型和大小

  • Python 使用列表推导式和简洁的语法

  • 循环语法不同:Java 使用 C 风格 for 循环,Python 使用 range

  • 数组/列表操作语法不同

十、相关题目

1. LeetCode 152. 乘积最大子数组

Java 解法

java

复制代码
class Solution {
    public int maxProduct(int[] nums) {
        if (nums.length == 0) return 0;
        
        int maxProd = nums[0];
        int minProd = nums[0];
        int result = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            int tempMax = Math.max(nums[i], Math.max(maxProd * nums[i], minProd * nums[i]));
            int tempMin = Math.min(nums[i], Math.min(maxProd * nums[i], minProd * nums[i]));
            
            maxProd = tempMax;
            minProd = tempMin;
            result = Math.max(result, maxProd);
        }
        
        return result;
    }
}
Python 解法

python

复制代码
from typing import List

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        max_prod = nums[0]
        min_prod = nums[0]
        result = nums[0]
        
        for i in range(1, len(nums)):
            temp_max = max(nums[i], max_prod * nums[i], min_prod * nums[i])
            temp_min = min(nums[i], max_prod * nums[i], min_prod * nums[i])
            
            max_prod, min_prod = temp_max, temp_min
            result = max(result, max_prod)
        
        return result

2. LeetCode 53. 最大子数组和

Java 解法

java

复制代码
class Solution {
    public int maxSubArray(int[] nums) {
        int maxSum = nums[0];
        int currentSum = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            currentSum = Math.max(nums[i], currentSum + nums[i]);
            maxSum = Math.max(maxSum, currentSum);
        }
        
        return maxSum;
    }
}
Python 解法

python

复制代码
from typing import List

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        max_sum = nums[0]
        current_sum = nums[0]
        
        for i in range(1, len(nums)):
            current_sum = max(nums[i], current_sum + nums[i])
            max_sum = max(max_sum, current_sum)
        
        return max_sum

十一、总结

核心要点

  1. 分解问题:将每个位置的乘积分解为前缀积和后缀积的乘积

  2. 避免重复计算:通过预处理前缀积和后缀积,避免重复计算

  3. 优化空间:可以使用输出数组存储前缀积,然后用一个变量计算后缀积

算法步骤(优化版本)

  1. 初始化输出数组 answer,所有元素为 1

  2. 从左到右计算前缀积,存储在 answer

  3. 从右到左计算后缀积,同时乘到 answer

  4. 返回 answer

时间复杂度与空间复杂度

  • 时间复杂度:O(n),需要两次遍历数组

  • 空间复杂度:O(1)(输出数组不计入空间复杂度)

适用场景

  • 需要计算除自身外所有元素的乘积

  • 不能使用除法

  • 要求时间复杂度和空间复杂度都尽可能低

扩展思考

这种前缀积和后缀积的思想可以应用到其他类似问题中,如:

  • 计算除自身外所有元素的和

  • 计算除自身外所有元素的最大值/最小值

  • 多维数组的类似问题

掌握前缀积和后缀积的解法,不仅能够解决除自身以外数组的乘积问题,还能够理解如何通过预处理来优化计算,是面试中非常重要的算法技巧。

相关推荐
Tisfy2 小时前
LeetCode 3651.带传送的最小路径成本:动态规划
算法·leetcode·动态规划·题解·排序
努力学习的小廉2 小时前
我爱学算法之—— 递归回溯综合(一)
算法·深度优先
m0_736919102 小时前
C++中的策略模式实战
开发语言·c++·算法
孞㐑¥2 小时前
算法—位运算
c++·经验分享·笔记·算法
软件算法开发2 小时前
基于卷尾猴优化的LSTM深度学习网络模型(CSA-LSTM)的一维时间序列预测算法matlab仿真
深度学习·算法·matlab·lstm·一维时间序列预测·卷尾猴优化·csa-lstm
高洁012 小时前
知识图谱如何在制造业实际落地应用
深度学习·算法·机器学习·数据挖掘·知识图谱
BHXDML2 小时前
数据结构:(二)逻辑之门——栈与队列
java·数据结构·算法
晚风吹长发2 小时前
初步了解Linux中的信号捕捉
linux·运维·服务器·c++·算法·进程·x信号
机器学习之心2 小时前
MATLAB基于GA-ELM与NSGA-Ⅱ算法的42CrMo表面激光熔覆参数多目标优化
算法·matlab·ga-elm