力扣hot100之238:除了自身以外数组的乘积
力扣hot100之238:除了自身以外数组的乘积

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

题目描述

给你一个整数数组 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]

因为数组中有 0,所以不能简单地依赖总乘积去计算。

这也是题目要求“不能使用除法”的重要原因之一。


解题思路

这道题最关键的点有两个:

  1. 不能使用除法

  2. 要在线性时间内完成

很多同学第一次看到这题,最直接的想法可能是:

  1. 先求整个数组的乘积 total

  2. 对于每个位置 i,答案就是 total / nums[i]

但这种做法有两个问题:

1. 题目明确禁止使用除法

这条路直接被封死了。

2. 如果数组中有 0,会很麻烦

比如:

nums = [1,2,0,4]

整个乘积就是 0,根本没法通过除法直接恢复每个位置的答案。

所以这题必须换一种思路。


核心思想:前缀积 + 后缀积

对于某个位置 i,我们要求的是:

nums[0] * nums[1] * ... * nums[i-1] * nums[i+1] * ... * nums[n-1]

也就是说,它其实可以拆成两部分:

  1. i 左边所有数的乘积

  2. i 右边所有数的乘积

如果我们能快速知道这两个乘积,那么答案就很容易得到:

answer[i] = 左边乘积 × 右边乘积

这就是这道题最核心的思路。


你的代码在做什么

你给出的代码非常经典,而且做了空间优化:

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int len = nums.length;
        if (len == 0) return new int[0];
        int[] ans = new int[len];
        ans[0] = 1;
        int tmp = 1;
        for (int i = 1; i < len; i++) {
            ans[i] = ans[i - 1] * nums[i - 1];
        }
        for (int i = len - 2; i >= 0; i--) {
            tmp *= nums[i + 1];
            ans[i] *= tmp;
        }
        return ans;
    }
}

它分成了两步:

  1. 第一遍从左往右,计算每个位置左边所有元素的乘积

  2. 第二遍从右往左,用变量 tmp 维护右边所有元素的乘积,再乘到答案中

最终就得到了每个位置“除自己以外”的乘积。


第一步:先算前缀积

代码

ans[0] = 1;
for (int i = 1; i < len; i++) {
    ans[i] = ans[i - 1] * nums[i - 1];
}

含义

这里的 ans[i] 在第一轮循环结束后,表示的是:

i 左边所有元素的乘积

为什么 ans[0] = 1

因为第 0 个位置左边没有任何元素。 空乘积按照惯例记为 1


举个例子

假设:

nums = [1,2,3,4]

那么第一轮计算后:

  • ans[0] = 1

  • ans[1] = ans[0] * nums[0] = 1 * 1 = 1

  • ans[2] = ans[1] * nums[1] = 1 * 2 = 2

  • ans[3] = ans[2] * nums[2] = 2 * 3 = 6

所以这时:

ans = [1,1,2,6]

它表示的其实就是:

  • 第 0 个位置左边乘积:1

  • 第 1 个位置左边乘积:1

  • 第 2 个位置左边乘积:1*2=2

  • 第 3 个位置左边乘积:1*2*3=6


第二步:再乘上后缀积

代码

int tmp = 1;
for (int i = len - 2; i >= 0; i--) {
    tmp *= nums[i + 1];
    ans[i] *= tmp;
}

含义

这里用变量 tmp 来维护:

当前下标 i 右边所有元素的乘积

然后把这个右边乘积乘到之前保存的左边乘积上:

ans[i] *= tmp;

于是最终:

ans[i] = 左边乘积 × 右边乘积

这正好就是题目要求的答案。


为什么 tmp 初始值是 1

因为最右边那个位置右边没有元素。

所以我们也把“空乘积”看作 1

这样在从右往左更新时,逻辑会非常统一。


第二轮详细推演

还是看这个例子:

nums = [1,2,3,4]

第一轮之后:

ans = [1,1,2,6]

然后开始第二轮:

初始:

tmp = 1

i = 2

tmp *= nums[3] = 4
tmp = 4
ans[2] *= tmp
ans[2] = 2 * 4 = 8

i = 1

tmp *= nums[2] = 3
tmp = 12
ans[1] *= tmp
ans[1] = 1 * 12 = 12

i = 0

tmp *= nums[1] = 2
tmp = 24
ans[0] *= tmp
ans[0] = 1 * 24 = 24

最终:

ans = [24,12,8,6]

这就是答案。


为什么最后一个位置不用在第二轮里更新

注意你的循环是:

for (int i = len - 2; i >= 0; i--)

也就是说,从倒数第二个位置开始。

因为对于最后一个位置来说:

  • 第一轮时,ans[len - 1] 已经是它左边所有元素的乘积

  • 而它右边没有元素,乘积为 1

所以它本身已经是正确答案,不需要在第二轮里额外更新。

这也是代码写得很巧的地方。


代码逐段解析

1. 获取数组长度并处理空数组

int len = nums.length;
if (len == 0) return new int[0];

如果数组为空,直接返回空数组。


2. 定义答案数组

int[] ans = new int[len];
ans[0] = 1;

ans 用来存最终结果。 先把第一个位置设为 1,表示它左边没有元素。


3. 第一轮:存前缀积

for (int i = 1; i < len; i++) {
    ans[i] = ans[i - 1] * nums[i - 1];
}

这一轮结束后,ans[i] 表示 i 左边所有元素的乘积。


4. 第二轮:乘上后缀积

int tmp = 1;
for (int i = len - 2; i >= 0; i--) {
    tmp *= nums[i + 1];
    ans[i] *= tmp;
}

tmp 动态维护从右往左的后缀积。


5. 返回答案

return ans;

为什么这题能做到 O(1) 额外空间

很多题解会说这题的空间复杂度是 O(1)(不算返回数组)。

这是因为:

  • ans 是题目要求返回的结果数组,不算额外空间

  • 除了 ans 以外,只用了一个变量 tmp

所以在题目的标准下,这种写法已经是空间优化版了。

如果你额外再开两个数组:

  • left[i] 存左边乘积

  • right[i] 存右边乘积

那虽然也能做出来,但空间复杂度会是 O(n)

你这份代码比那种写法更进一步。


和“双数组写法”对比

双数组写法

思路是:

  • left[i] 记录左边乘积

  • right[i] 记录右边乘积

  • 最后 ans[i] = left[i] * right[i]

优点:

  • 非常直观

  • 容易理解

缺点:

  • 需要两个额外数组,空间复杂度是 O(n)


当前这种写法

思路是:

  • ans 先存左边乘积

  • 再用一个变量 tmp 维护右边乘积并乘进去

优点:

  • 时间复杂度 O(n)

  • 额外空间复杂度更低

  • 代码更简洁

缺点:

  • 第一次看时,不如双数组写法直观


复杂度分析

设数组长度为 n

时间复杂度

O(n)

总共只遍历了两次数组。


空间复杂度

O(1)

不算返回数组 ans 的情况下,只使用了常数个额外变量。

如果把返回数组也算进去,那么是 O(n)


总结

这道题的核心就在于把问题拆成两部分:

某个位置的答案 = 左边所有元素乘积 × 右边所有元素乘积

然后:

  1. 第一遍从左往右,把每个位置左边的乘积先存进 ans

  2. 第二遍从右往左,用 tmp 维护右边乘积,再乘到 ans

所以你可以把这题记成一句话:

先存前缀积,再动态乘上后缀积。

这是一道非常经典的数组题,也是“前缀思想 + 空间优化”结合得非常漂亮的一题,值得彻底掌握。


如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇