Files
interview/16-LeetCode Hot 100/除自身以外数组的乘积.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

15 KiB
Raw Blame History

除自身以外数组的乘积 (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),不考虑输出数组

问题分析

  1. 效率低n=10⁵ 时n² 不可接受
  2. 重复计算:很多乘积被多次计算
  3. 无法利用已知信息

优化思考 - 第一步:使用除法

观察:如果可以使用除法

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 之后所有元素的乘积
  • 两部分独立计算,然后相乘

优化后的思路

# 预计算左侧乘积
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. 分解问题的思想

    • 复杂问题 → 简单子问题
    • 每个位置的答案 = 左边所有数的乘积 × 右边所有数的乘积
  2. 独立计算的优势

    • 左侧乘积可以从左到右递推计算
    • 右侧乘积可以从右到左递推计算
    • 两部分互不干扰
  3. 空间优化的技巧

    • 用输出数组存储左侧乘积
    • 用变量累积右侧乘积
    • 边计算边更新,避免额外数组

详细算法流程

步骤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] = 10 左侧没有元素)
  • 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
}

关键点

  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使用除法

错误代码

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索引错误

错误代码

// 错误:包含了当前元素
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

思路

  1. 统计 0 的个数
  2. 如果超过 1 个 0全部为 0
  3. 如果正好 1 个 0只有该位置为总乘积其他为 0
  4. 如果没有 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 加分项

深度理解

  • 分离思想:将复杂问题分解为简单的子问题
  • 空间优化:利用输出数组,避免额外空间
  • 递推关系:左右乘积都可以递推计算

实战扩展

  • 前缀和/后缀和:类似思想,用加法代替乘法
  • 范围查询:线段树、树状数组
  • 业务场景:计算贡献度、权重分配

变形题目

  1. 152. 乘积最大子数组
  2. 前缀和问题
  3. 区间乘积查询

总结

核心要点

  1. 分离思想:将 answer[i] 分解为左侧 × 右侧
  2. 递推计算:左侧和右侧都可以递推计算
  3. 空间优化:用输出数组存储左侧,变量累积右侧

易错点

  • 索引错误(是否包含当前元素)
  • 更新顺序错误(先使用还是先更新)
  • 边界条件0 的处理)

最优解法:分离左右乘积 + 空间优化,时间 O(n),空间 O(1)