按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
15 KiB
除自身以外数组的乘积 (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) 的额外空间复杂度(输出数组不被视为额外空间)内完成此题目吗?
思路推导
暴力解法分析
最直观的思路:对于每个位置,计算其他所有元素的乘积。
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),不考虑输出数组
问题分析:
- 效率低:n=10⁵ 时,n² 不可接受
- 重复计算:很多乘积被多次计算
- 无法利用已知信息
优化思考 - 第一步:使用除法
观察:如果可以使用除法
total_product = 1
for num in nums:
total_product *= num
answer[i] = total_product / nums[i]
问题:
- 题目禁止使用除法
- 如果 nums[i] = 0,除法会出错
- 如果多个 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 之后所有元素的乘积
- 两部分独立计算,然后相乘
优化后的思路:
# 预计算左侧乘积
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) 额外空间(输出数组除外)
关键优化:用输出数组存储左侧乘积,用变量累积右侧乘积
# 用 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:计算左侧乘积(存储在 answer 中)
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:计算右侧乘积并更新答案
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?
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]?
# 错误写法
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:为什么从右向左遍历?
# 正确:从右向左
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?
# 正确顺序
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
代码实现
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
}
关键点:
- 用
answer存储左侧乘积 - 用
right变量累积右侧乘积 - 从右向左遍历,边计算边更新
执行过程演示
输入: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:使用除法
❌ 错误代码:
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
}
问题:
- 题目明确禁止使用除法
- 如果
num = 0,除法会出错 - 无法处理多个 0 的情况
错误2:索引错误
❌ 错误代码:
// 错误:包含了当前元素
answer[i] = answer[i-1] * nums[i] // 错误!
// 正确:不包含当前元素
answer[i] = answer[i-1] * nums[i-1] // 正确
原因:
answer[i]是 nums[i] 左侧的乘积- 不应该包含 nums[i] 本身
错误3:更新顺序错误
❌ 错误代码:
// 错误:先更新了 right
right *= nums[i]
answer[i] *= right // right 包含了 nums[i]
✅ 正确代码:
// 正确:先使用 right,再更新
answer[i] *= right
right *= nums[i]
原因:
right应该是 nums[i] 右侧的乘积- 不应该包含 nums[i] 本身
进阶问题
Q1: 如果允许使用除法,如何处理 0?
思路:
- 统计 0 的个数
- 如果超过 1 个 0,全部为 0
- 如果正好 1 个 0,只有该位置为总乘积,其他为 0
- 如果没有 0,正常计算
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: 如何处理大数溢出?
思路:使用对数转换
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 加分项
深度理解
- 分离思想:将复杂问题分解为简单的子问题
- 空间优化:利用输出数组,避免额外空间
- 递推关系:左右乘积都可以递推计算
实战扩展
- 前缀和/后缀和:类似思想,用加法代替乘法
- 范围查询:线段树、树状数组
- 业务场景:计算贡献度、权重分配
变形题目
- 152. 乘积最大子数组
- 前缀和问题
- 区间乘积查询
总结
核心要点:
- 分离思想:将 answer[i] 分解为左侧 × 右侧
- 递推计算:左侧和右侧都可以递推计算
- 空间优化:用输出数组存储左侧,变量累积右侧
易错点:
- 索引错误(是否包含当前元素)
- 更新顺序错误(先使用还是先更新)
- 边界条件(0 的处理)
最优解法:分离左右乘积 + 空间优化,时间 O(n),空间 O(1)