leetcode 面试经典 150 题(13/150) 238. 除自身以外数组的乘积

103 阅读3分钟

题目描述

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

示例 1:

输出: [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]
解释: 
- 存在 0 元素时,除自身外其他元素的乘积为 0
- answer[2] 对应位置无 0,乘积为 (-1)×1×(-3)×3 = 9

算法思路

两次遍历法:通过两次遍历分别计算每个元素的左边乘积和右边乘积,最终合并结果。

核心思想

  1. 左乘积遍历:从左到右遍历数组,计算每个元素左侧所有元素的乘积,存入结果数组。
  2. 右乘积遍历:从右到左遍历数组,计算每个元素右侧所有元素的乘积,并与左乘积相乘得到最终结果。

具体步骤

  1. 初始化结果数组ans[i] 初始为左侧所有元素的乘积。
  2. 第一次遍历(左→右)
    • ans[i] = ans[i-1] * nums[i-1],逐步累乘左侧元素。
  3. 第二次遍历(右→左)
    • 用临时变量 tmp 存储右侧元素的累乘结果。
    • ans[i] *= tmp,将左右乘积合并。

复杂度分析

  • 时间复杂度:O(n),两次线性遍历数组。
  • 空间复杂度:O(1)(输出数组不计入空间复杂度)。

代码实现

func productExceptSelf(nums []int) []int {
    n := len(nums)
    if n == 0 {
        return []int{}
    }

    ans := make([]int, n)
    ans[0] = 1 // 第一个元素左侧无元素,乘积为1

    // 计算左侧乘积
    for i := 1; i < n; i++ {
        ans[i] = ans[i-1] * nums[i-1]
    }

    tmp := 1 // 临时变量存储右侧乘积
    // 计算右侧乘积并合并结果
    // 左侧乘积计算完成后,最后一位已经确定,所以要从倒数第二位开始更新,也就是 n - 2
    for i := n-2; i >= 0; i-- {
        tmp *= nums[i+1]
        ans[i] *= tmp
    }

    return ans
}

关键点总结

  1. 两次遍历策略

    • 左遍历计算每个元素左侧的累积乘积。
    • 右遍历动态计算右侧乘积并直接合并到结果中。
  2. 空间优化

    • 利用结果数组 ans 存储中间结果,避免使用额外空间。
    • 通过临时变量 tmp 减少右侧乘积的计算开销。
  3. 边界处理

    • 数组长度为0时直接返回空数组。
    • 第一个元素的左侧乘积初始化为1(无左侧元素)。

示例解析

示例1:nums = [1,2,3,4]

  1. 左遍历
    • ans = [1, 1×1=1, 1×2=2, 2×3=6][1,1,2,6]
  2. 右遍历
    • i=2: tmp=4 → ans[2]=2×4=8
    • i=1: tmp=4 × 3=12 → ans[1]=1×12=12
    • i=0: tmp=12 × 2=24 → ans[0]=1×24=24
    • 最终结果:[24,12,8,6]

示例2:nums = [-1,1,0,-3,3]

  1. 左遍历
    • ans = [1, -1, -1×1=-1, -1×0=0, 0×(-3)=0]
  2. 右遍历
    • i=3: tmp=3 → ans[3]=0×3=0
    • i=2: tmp=3 × (-3)=-9 → ans[2]=-1 × (-9)=9
    • i=1: tmp=-9 × 0=0 → ans[1]=-1 × 0=0
    • i=0: tmp=0 × 1=0 → ans[0]=1 × 0=0
    • 最终结果:[0,0,9,0,0]