# 除自身以外数组的乘积 (Product of Array Except Self) LeetCode 238. Medium ## 题目描述 给你一个整数数组 `nums`,返回数组 `answer`,其中 `answer[i]` 等于 `nums` 中除 `nums[i]` 之外其余各元素的乘积。 **题目要求**:请不要使用除法,且在 O(n) 时间复杂度内完成此题。 **示例 1**: ``` 输入: nums = [1,2,3,4] 输出: [24,12,8,6] 解释: - answer[0] = 2 * 3 * 4 = 24 - answer[1] = 1 * 3 * 4 = 12 - answer[2] = 1 * 2 * 4 = 8 - answer[3] = 1 * 2 * 3 = 6 ``` **示例 2**: ``` 输入: nums = [-1,1,0,-3,3] 输出: [0,0,9,0,0] ``` **进阶**:你可以在 O(1) 的额外空间复杂度(输出数组不被视为额外空间)内完成此题目吗? ## 思路推导 ### 暴力解法分析 **最直观的思路**:对于每个位置,计算其他所有元素的乘积。 ```python def productExceptSelf(nums): n = len(nums) answer = [] for i in range(n): product = 1 for j in range(n): if j != i: product *= nums[j] answer.append(product) return answer ``` **时间复杂度**:O(n²) - 外层循环:O(n) - 内层循环:O(n) - 总计:O(n) × O(n) = O(n²) **空间复杂度**:O(1),不考虑输出数组 **问题分析**: 1. 效率低:n=10⁵ 时,n² 不可接受 2. 重复计算:很多乘积被多次计算 3. 无法利用已知信息 ### 优化思考 - 第一步:使用除法 **观察**:如果可以使用除法 ```python total_product = 1 for num in nums: total_product *= num answer[i] = total_product / nums[i] ``` **问题**: 1. 题目禁止使用除法 2. 如果 nums[i] = 0,除法会出错 3. 如果多个 0,需要特殊处理 ### 优化思考 - 第二步:分离左右乘积 **关键观察**:`answer[i] = 左侧乘积 × 右侧乘积` ``` nums = [1, 2, 3, 4] answer[0] = (空) × (2 × 3 × 4) = 左侧[0] × 右侧[0] answer[1] = (1) × (3 × 4) = 左侧[1] × 右侧[1] answer[2] = (1 × 2) × (4) = 左侧[2] × 右侧[2] answer[3] = (1 × 2 × 3) × (空) = 左侧[3] × 右侧[3] ``` **为什么这样思考?** - 每个位置的答案可以分解为两部分 - 左侧部分:i 之前所有元素的乘积 - 右侧部分:i 之后所有元素的乘积 - 两部分独立计算,然后相乘 **优化后的思路**: ```python # 预计算左侧乘积 left = [1] * n for i in range(1, n): left[i] = left[i-1] * nums[i-1] # 预计算右侧乘积 right = [1] * n for i in range(n-2, -1, -1): right[i] = right[i+1] * nums[i+1] # 合并结果 answer = [left[i] * right[i] for i in range(n)] ``` **时间复杂度**:O(n) - 计算左侧:O(n) - 计算右侧:O(n) - 合并结果:O(n) - 总计:O(n) **空间复杂度**:O(n) - 左侧数组:O(n) - 右侧数组:O(n) - 输出数组:O(n) - 总计:O(n) ### 优化思考 - 第三步:空间优化 **问题**:题目要求 O(1) 额外空间(输出数组除外) **关键优化**:用输出数组存储左侧乘积,用变量累积右侧乘积 ```python # 用 answer 存储左侧乘积 answer = [1] * n for i in range(1, n): answer[i] = answer[i-1] * nums[i-1] # 用变量累积右侧乘积,直接更新 answer right = 1 for i in range(n-1, -1, -1): answer[i] *= right right *= nums[i] ``` **为什么这样思考?** - 输出数组不被视为额外空间 - 右侧乘积可以用一个变量累积 - 从右向左遍历时,边计算边更新 **空间复杂度**:O(1) - 只用了一个变量 `right` - 输出数组 `answer` 不计入 ## 解题思路 ### 核心思想 **分离左右乘积**:将 `answer[i]` 分解为左侧乘积和右侧乘积的乘积。 **为什么这样思考?** 1. **分解问题的思想**: - 复杂问题 → 简单子问题 - 每个位置的答案 = 左边所有数的乘积 × 右边所有数的乘积 2. **独立计算的优势**: - 左侧乘积可以从左到右递推计算 - 右侧乘积可以从右到左递推计算 - 两部分互不干扰 3. **空间优化的技巧**: - 用输出数组存储左侧乘积 - 用变量累积右侧乘积 - 边计算边更新,避免额外数组 ### 详细算法流程 **步骤1:计算左侧乘积(存储在 answer 中)** ```python answer = [1] * n # answer[i] = nums[0] × ... × nums[i-1] for i in range(1, n): answer[i] = answer[i-1] * nums[i-1] ``` **关键点**: - `answer[0] = 1`(0 左侧没有元素) - `answer[i]` 依赖于 `answer[i-1]` - 递推关系:`answer[i] = answer[i-1] × nums[i-1]` **示例**: ``` nums = [1, 2, 3, 4] i=0: answer[0] = 1 i=1: answer[1] = answer[0] × nums[0] = 1 × 1 = 1 i=2: answer[2] = answer[1] × nums[1] = 1 × 2 = 2 i=3: answer[3] = answer[2] × nums[2] = 2 × 3 = 6 answer = [1, 1, 2, 6] (左侧乘积) ``` **步骤2:计算右侧乘积并更新答案** ```python right = 1 for i in range(n-1, -1, -1): # answer[i] *= 右侧乘积 answer[i] *= right # 更新右侧乘积 right *= nums[i] ``` **关键点**: - `right` 初始为 1(最右侧右侧没有元素) - 从右向左遍历 - 递推关系:`right = right × nums[i]` **示例(续)**: ``` nums = [1, 2, 3, 4] answer = [1, 1, 2, 6] (左侧乘积) i=3: answer[3] = 6 × 1 = 6, right = 1 × 4 = 4 i=2: answer[2] = 2 × 4 = 8, right = 4 × 3 = 12 i=1: answer[1] = 1 × 12 = 12, right = 12 × 2 = 24 i=0: answer[0] = 1 × 24 = 24, right = 24 × 1 = 24 answer = [24, 12, 8, 6] (最终结果) ``` ### 关键细节说明 **细节1:为什么 answer[0] = 1?** ```python answer = [1] * n # 所有位置初始化为 1 ``` **原因**: - `answer[0]` 表示 nums[0] 左侧所有元素的乘积 - nums[0] 左侧没有元素 - 空乘积定义为 1(乘法的单位元) - 类似:`answer[n-1]` 右侧也没有元素,right 初始为 1 **细节2:为什么是 `answer[i] = answer[i-1] × nums[i-1]`?** ```python # 错误写法 answer[i] = answer[i-1] * nums[i] # 错误!会包含当前元素 # 正确写法 answer[i] = answer[i-1] * nums[i-1] # 正确 ``` **原因**: - `answer[i]` 是 nums[i] 左侧的乘积 - 不应该包含 nums[i] 本身 - 示例: ``` nums = [1, 2, 3, 4] answer[2] = nums[0] × nums[1] = 1 × 2 = 2 不包含 nums[2] = 3 ``` **细节3:为什么从右向左遍历?** ```python # 正确:从右向左 for i in range(n-1, -1, -1): answer[i] *= right right *= nums[i] # 错误:从左向右 for i in range(n): answer[i] *= right # right 值不对 right *= nums[i] ``` **原因**: - `right` 是 nums[i] 右侧的乘积 - 必须先计算右侧的值 - 从右向左遍历才能保证 `right` 是正确的 **细节4:为什么顺序是先更新 answer,再更新 right?** ```python # 正确顺序 answer[i] *= right # 先使用当前的 right right *= nums[i] # 再更新 right # 错误顺序 right *= nums[i] # 错误!先更新了 answer[i] *= right # right 包含了 nums[i] 本身 ``` **原因**: - `right` 应该是 nums[i] 右侧的乘积 - 不应该包含 nums[i] 本身 - 必须先使用当前的 `right`,再更新 ### 边界条件分析 **边界1:数组长度为 1** ``` 输入:nums = [5] 输出:[1] 解释: answer[0] = 1(左侧为空) right = 1(右侧为空) answer[0] = 1 × 1 = 1 ``` **边界2:包含 0** ``` 输入:nums = [1, 0, 3, 4] 过程: answer = [1, 1, 0, 0] (左侧乘积) i=3: answer[3] = 0 × 1 = 0, right = 4 i=2: answer[2] = 0 × 4 = 0, right = 12 i=1: answer[1] = 1 × 12 = 12, right = 0 i=0: answer[0] = 1 × 0 = 0, right = 1 输出:[0, 12, 0, 0] 验证: answer[0] = 0 × 3 × 4 = 0 ✓ answer[1] = 1 × 3 × 4 = 12 ✓ answer[2] = 1 × 0 × 4 = 0 ✓ answer[3] = 1 × 0 × 3 = 0 ✓ ``` **边界3:包含多个 0** ``` 输入:nums = [0, 1, 0, 3] 过程: answer = [1, 0, 0, 0] (左侧乘积) i=3: answer[3] = 0 × 1 = 0, right = 3 i=2: answer[2] = 0 × 3 = 0, right = 0 i=1: answer[1] = 0 × 0 = 0, right = 0 i=0: answer[0] = 1 × 0 = 0, right = 0 输出:[0, 0, 0, 0] 验证: 任意位置都会乘到至少一个 0 ``` **边界4:负数** ``` 输入:nums = [-1, -2, -3, -4] 过程: answer = [1, -1, 2, -6] (左侧乘积) i=3: answer[3] = -6 × 1 = -6, right = -4 i=2: answer[2] = 2 × (-4) = -8, right = 12 i=1: answer[1] = -1 × 12 = -12, right = -24 i=0: answer[0] = 1 × (-24) = -24, right = 24 输出:[-24, -12, -8, -6] 验证: answer[0] = (-2) × (-3) × (-4) = -24 ✓ answer[1] = (-1) × (-3) × (-4) = -12 ✓ answer[2] = (-1) × (-2) × (-4) = -8 ✓ answer[3] = (-1) × (-2) × (-3) = -6 ✓ ``` ### 复杂度分析(详细版) **时间复杂度**: ``` - 计算左侧乘积:O(n),遍历一次 - 计算右侧乘积:O(n),遍历一次 - 总计:O(n) + O(n) = O(n) 为什么是 O(n)? - 两次线性扫描 - 每个元素只访问一次 - 常数操作(乘法和赋值) ``` **空间复杂度**: ``` - 输出数组:O(n),不计入额外空间 - 右侧变量:O(1) - 循环变量:O(1) - 总计:O(1)(满足题目要求) ``` --- ## 图解过程 ``` nums = [1, 2, 3, 4] 步骤1:计算左侧乘积 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] 图示: nums: [1, 2, 3, 4] ↓ answer: [1, 1, 2, 6] (×1)(×1)(×2)(×6) 左侧累积 步骤2:计算右侧乘积并更新 right = 1 i=3: answer[3] = 6 × 1 = 6, right = 1 × 4 = 4 i=2: answer[2] = 2 × 4 = 8, right = 4 × 3 = 12 i=1: answer[1] = 1 × 12 = 12, right = 12 × 2 = 24 i=0: answer[0] = 1 × 24 = 24, right = 24 × 1 = 24 answer = [24, 12, 8, 6] 图示: nums: [1, 2, 3, 4] ↓ answer: [24, 12, 8, 6] (×24)(×12)(×8)(×6) 右侧累积 最终结果: answer[0] = 1 × (2 × 3 × 4) = 24 answer[1] = (1) × (3 × 4) = 12 answer[2] = (1 × 2) × (4) = 8 answer[3] = (1 × 2 × 3) × 1 = 6 ``` --- ## 代码实现 ```go func productExceptSelf(nums []int) []int { n := len(nums) answer := make([]int, n) // 步骤1:计算左侧乘积 answer[0] = 1 for i := 1; i < n; i++ { answer[i] = answer[i-1] * nums[i-1] } // 步骤2:计算右侧乘积并更新 right := 1 for i := n - 1; i >= 0; i-- { answer[i] = answer[i] * right right *= nums[i] } return answer } ``` **关键点**: 1. 用 `answer` 存储左侧乘积 2. 用 `right` 变量累积右侧乘积 3. 从右向左遍历,边计算边更新 --- ## 执行过程演示 **输入**:nums = [1, 2, 3, 4] ``` 初始化:answer = [0, 0, 0, 0], right = 1 步骤1:计算左侧乘积 i=0: answer[0] = 1 i=1: answer[1] = answer[0] × nums[0] = 1 × 1 = 1 i=2: answer[2] = answer[1] × nums[1] = 1 × 2 = 2 i=3: answer[3] = answer[2] × nums[2] = 2 × 3 = 6 answer = [1, 1, 2, 6] 步骤2:计算右侧乘积并更新 i=3: answer[3] = 6 × 1 = 6, right = 1 × 4 = 4 i=2: answer[2] = 2 × 4 = 8, right = 4 × 3 = 12 i=1: answer[1] = 1 × 12 = 12, right = 12 × 2 = 24 i=0: answer[0] = 1 × 24 = 24, right = 24 × 1 = 24 answer = [24, 12, 8, 6] 验证: answer[0] = 2 × 3 × 4 = 24 ✓ answer[1] = 1 × 3 × 4 = 12 ✓ answer[2] = 1 × 2 × 4 = 8 ✓ answer[3] = 1 × 2 × 3 = 6 ✓ ``` --- ## 常见错误 ### 错误1:使用除法 ❌ **错误代码**: ```go func productExceptSelf(nums []int) []int { total := 1 for _, num := range nums { total *= num } answer := make([]int, len(nums)) for i, num := range nums { answer[i] = total / num // 错误!题目禁止除法 } return answer } ``` **问题**: 1. 题目明确禁止使用除法 2. 如果 `num = 0`,除法会出错 3. 无法处理多个 0 的情况 --- ### 错误2:索引错误 ❌ **错误代码**: ```go // 错误:包含了当前元素 answer[i] = answer[i-1] * nums[i] // 错误! // 正确:不包含当前元素 answer[i] = answer[i-1] * nums[i-1] // 正确 ``` **原因**: - `answer[i]` 是 nums[i] 左侧的乘积 - 不应该包含 nums[i] 本身 --- ### 错误3:更新顺序错误 ❌ **错误代码**: ```go // 错误:先更新了 right right *= nums[i] answer[i] *= right // right 包含了 nums[i] ``` ✅ **正确代码**: ```go // 正确:先使用 right,再更新 answer[i] *= right right *= nums[i] ``` **原因**: - `right` 应该是 nums[i] 右侧的乘积 - 不应该包含 nums[i] 本身 --- ## 进阶问题 ### Q1: 如果允许使用除法,如何处理 0? **思路**: 1. 统计 0 的个数 2. 如果超过 1 个 0,全部为 0 3. 如果正好 1 个 0,只有该位置为总乘积,其他为 0 4. 如果没有 0,正常计算 ```go func productExceptSelfWithDivision(nums []int) []int { n := len(nums) answer := make([]int, n) // 统计 0 的个数和总乘积 zeroCount := 0 totalProduct := 1 for _, num := range nums { if num == 0 { zeroCount++ } else { totalProduct *= num } } // 根据零的个数处理 for i, num := range nums { if zeroCount > 1 { answer[i] = 0 } else if zeroCount == 1 { if num == 0 { answer[i] = totalProduct } else { answer[i] = 0 } } else { answer[i] = totalProduct / num } } return answer } ``` --- ### Q2: 如何处理大数溢出? **思路**:使用对数转换 ```go func productExceptSelfLog(nums []int) []int { n := len(nums) logSum := make([]float64, n) // 计算对数和 for i, num := range nums { logSum[i] = math.Log(float64(num)) } answer := make([]int, n) totalLog := 0.0 for _, log := range logSum { totalLog += log } // 转换回整数 for i, log := range logSum { answer[i] = int(math.Exp(totalLog - log) + 0.5) } return answer } ``` **注意**: - 对数转换会损失精度 - 适用于浮点数场景 - 整数场景需要其他方法 --- ## P7 加分项 ### 深度理解 - **分离思想**:将复杂问题分解为简单的子问题 - **空间优化**:利用输出数组,避免额外空间 - **递推关系**:左右乘积都可以递推计算 ### 实战扩展 - **前缀和/后缀和**:类似思想,用加法代替乘法 - **范围查询**:线段树、树状数组 - **业务场景**:计算贡献度、权重分配 ### 变形题目 1. [152. 乘积最大子数组](https://leetcode.cn/problems/maximum-product-subarray/) 2. 前缀和问题 3. 区间乘积查询 --- ## 总结 **核心要点**: 1. **分离思想**:将 answer[i] 分解为左侧 × 右侧 2. **递推计算**:左侧和右侧都可以递推计算 3. **空间优化**:用输出数组存储左侧,变量累积右侧 **易错点**: - 索引错误(是否包含当前元素) - 更新顺序错误(先使用还是先更新) - 边界条件(0 的处理) **最优解法**:分离左右乘积 + 空间优化,时间 O(n),空间 O(1)