LeetCode 热题 HOT 100(普通数组)238. 除自身以外数组的乘积

38 阅读4分钟

题目描述

238. 除自身以外数组的乘积

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内。

请 不要使用除法, 且在 O(n) 时间复杂度内完成此题。

 

示例 1:

输入: nums = [1,2,3,4]
输出: [24,12,8,6]

示例 2:

输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]

 

提示:

  • 2 <= nums.length <= 105
  • -30 <= nums[i] <= 30
  • 输入 保证 数组 answer[i] 在  32 位 整数范围内

 

进阶: 你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)

主要思路

  1. 不能使用除法,所以不能先计算所有元素的乘积,然后除以当前元素
  2. 考虑将问题分解,对于每个位置 i,其结果等于其左侧所有元素的乘积乘以右侧所有元素的乘积
  3. 可以通过两次遍历实现:
  • 第一次从左到右,计算每个位置左侧元素的乘积
  • 第二次从右到左,计算每个位置右侧元素的乘积并与左侧乘积相乘

代码实现

func productExceptSelf(nums []int) []int {
    n := len(nums)
    answer := make([]int, n)
    
    answer[0] = 1
    // 计算每个元素所有左侧元素乘积(前缀乘积)
    for i := 1; i < n; i++ {
        answer[1] = answer[i-1] * nums[i-1]
    }
    // answer[1] = nums[0]
    // answer[2] = nums[1] * nums[0]
    // answer[3] = nums[2] * nums[1] * nums[0]
    
    // 计算每个元素右侧元素乘积(后缀乘积)
    // 右侧乘积 rightProduct
    rightProduct := 1
    // i = 3, rightProduct = 1, answer[3] = nums[2] * nums[1] * nums[0]
    // i = 2, rightProduct = nums[3], answer[2] = nums[1] * nums[0]
    // i = 1, rightProduct = nums[3] * nums[2], answer[1] = nums[0]
    // i = 0, rightProduct = nums[3] * nums[2] * nums[1], answer[0] = 1
    for i := n-1; i >= 0; i-- {
        answer[i] = answer[i] * rightProduct
        rightProct = rightProduct * nums[i]
    }
    return answer
}

执行时序图

sequenceDiagram
    participant main as 主函数
    participant answer as 结果数组
    participant leftPass as 左侧遍历
    participant rightPass as 右侧遍历
    
    main->>answer: 初始化结果数组
    main->>leftPass: 开始从左到右遍历
    
    leftPass->>answer: 设置 answer[0] = 1
    
    loop 从索引1到末尾
        leftPass->>answer: 计算当前位置左侧乘积
        Note right of answer: answer[i] = answer[i-1] * nums[i-1]
    end
    
    leftPass-->>main: 左侧遍历完成
    main->>rightPass: 开始从右到左遍历
    
    rightPass->>rightPass: 初始化 rightProduct = 1
    
    loop 从末尾到索引0
        rightPass->>answer: 将左侧乘积与右侧乘积相乘
        Note right of answer: answer[i] = answer[i] * rightProduct
        rightPass->>rightPass: 更新右侧乘积
        Note right of rightPass: rightProduct = rightProduct * nums[i]
    end
    
    rightPass-->>main: 右侧遍历完成
    main-->>main: 返回结果数组

左侧遍历过程关键状态变化图

索引nums[i]answer[i](左侧乘积)计算过程
011初始化为 1
121answer[1] = answer[0] * nums[0] = 1 * 1 = 1
232answer[2] = answer[1] * nums[1] = 1 * 2 = 2
346answer[3] = answer[2] * nums[2] = 2 * 3 = 6

左侧遍历后,answer = [1, 1, 2, 6]

右侧遍历过程关键状态变化图

索引nums[i]rightProductanswer[i](左侧 * 右侧)计算过程
3416answer[3] = answer[3] * rightProduct = 6 * 1 = 6
2348answer[2] = answer[2] * rightProduct = 2 * 4 = 8
121212answer[1] = answer[1] * rightProduct = 1 * 12 = 12
012424answer[0] = answer[0] * rightProduct = 1 * 24 = 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. 数组遍历技巧:从两个方向(左到右和右到左)遍历数组,构建结果。
  2. 前缀和后缀乘积:理解如何通过前缀乘积和后缀乘积来解决问题。
  3. 不使用除法的乘积计算:在不能使用除法的限制下,如何计算除自身外的乘积。
  4. 空间复杂度优化:使用 O(1) 的额外空间(不包括输出数组)完成算法。
  5. 时间复杂度控制:在 O(n) 时间内完成计算。
  6. 技巧应用:在第二次遍历中,使用一个变量 rightProduct 动态维护右侧乘积。
  7. 边界条件处理:正确初始化第一个元素和最后一个元素的处理。
  8. 整数溢出考虑:题目保证结果在32位整数范围内,但计算过程中需要注意可能的溢出问题。
  9. 代码简洁性:使用简洁的代码实现复杂的逻辑。

这道题展示了一种典型的数组问题解决方案:通过多次遍历来构建结果,而不是使用暴力的 O(n²) 方法。空间复杂度的优化也是一个重要的考点,通过使用一个变量来替代额外的数组,使得空间复杂度降至 O(1)。