LeetCode 238 除自身以外数组的乘积:前缀积与后缀积详解

前言

这道题最容易想到的办法,大概都是先把数组中所有元素乘起来,再用总乘积除以当前位置的元素。

思路看起来没有问题,题目却直接补了一句:不能使用除法。

如果数组中还可能出现 0,总乘积除以当前元素这条路本身也会变得麻烦。第一次做到这里时,很容易把注意力一直放在"怎样从总乘积中去掉 nums[i]"上。实际上,换一个角度会简单很多:既然不能把当前元素除掉,那就从一开始不让它参与计算。

对于位置 i 来说,除 nums[i] 以外的元素只有两部分:它左边的元素和它右边的元素。因此:

复制代码
answer[i] = i 左边所有元素的乘积 × i 右边所有元素的乘积

这就是前缀积与后缀积解法的出发点。

一、题目说明

给定一个整数数组 nums,返回数组 answer,其中:

复制代码
answer[i] = nums 中除 nums[i] 以外其余所有元素的乘积

题目还有两个重要限制:

  • 不能使用除法;

  • 需要在 O(n) 时间复杂度内完成。

例如:

复制代码
输入: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

二、暴力做法为什么不合适

如果不考虑题目的时间要求,可以为每个位置重新遍历整个数组,跳过当前位置,将其他元素全部相乘。

数组长度为 n 时,一共有 n 个位置;每个位置又要遍历接近 n 个元素,因此时间复杂度是:

复制代码
O(n²)

这种写法不难,但重复计算很多。例如,在计算 answer[2] 和 answer[3] 时,前面的 1 × 2 被反复计算了。题目要求 O(n),实际上就是在提醒我们:这些重复的乘积应该提前保存下来。

三、关键转化:把答案拆成左右两部分

仍然以数组 [1,2,3,4] 为例。

如果要计算下标 2 对应的答案,也就是排除元素 3,剩余元素可以分为:

复制代码
左侧:[1,2]
右侧:[4]

所以:

复制代码
answer[2] = (1 × 2) × 4 = 8

这时可以引入两个数组:

  • pre[i]:nums[i] 左侧所有元素的乘积;

  • suf[i]:nums[i] 右侧所有元素的乘积。

那么每个位置的答案就是:

复制代码
res[i] = pre[i] * suf[i];

注意,pre[i] 和 suf[i] 都不包含 nums[i] 本身。这一点是整个定义中最重要的地方。

四、前缀积是怎样计算的

前缀积从左向右计算:

复制代码
pre[0] = 1;
for (int i = 1; i < n; i++) {
    pre[i] = pre[i - 1] * nums[i - 1];
}

为什么公式中乘的是 nums[i - 1],而不是 nums[i]?

因为 pre[i] 只保存下标 i 左侧元素的乘积,不能包含当前位置。pre[i - 1] 已经包含了 i - 1 左侧的全部元素,再乘上 nums[i - 1],刚好得到 i 左侧所有元素的乘积。

对于 [1,2,3,4]:

复制代码
pre[0] = 1
pre[1] = pre[0] × nums[0] = 1 × 1 = 1
pre[2] = pre[1] × nums[1] = 1 × 2 = 2
pre[3] = pre[2] × nums[2] = 2 × 3 = 6

所以:

复制代码
pre = [1,1,2,6]

五、后缀积是怎样计算的

后缀积与前缀积完全对称,只不过需要从右向左计算:

复制代码
suf[n - 1] = 1;
for (int i = n - 2; i >= 0; i--) {
    suf[i] = suf[i + 1] * nums[i + 1];
}

suf[i] 只包含下标 i 右侧的元素,所以使用的是 nums[i + 1]。

对于 [1,2,3,4]:

复制代码
suf[3] = 1
suf[2] = suf[3] × nums[3] = 1 × 4 = 4
suf[1] = suf[2] × nums[2] = 4 × 3 = 12
suf[0] = suf[1] × nums[1] = 12 × 2 = 24

最终得到:

复制代码
suf = [24,12,4,1]

六、为什么两端要初始化为 1

这段代码里最容易让人疑惑的是:

复制代码
pre[0] = 1;
suf[n - 1] = 1;

nums[0] 的左侧没有任何元素,nums[n - 1] 的右侧也没有任何元素。这里不能初始化为 0,因为任何数乘以 0 都会变成 0,后续结果会全部受到影响。

乘法中的单位元是 1:

复制代码
x × 1 = x

因此,可以把"空的一侧"的乘积理解为 1。这样既符合乘法规则,也让首尾位置可以使用与其他位置完全相同的计算公式。

七、完整演算

把三个数组放在一起看,逻辑会更直观:

下标 i nums[i] 左侧乘积 pre[i] 右侧乘积 suf[i] res[i]
0 1 1 24 24
1 2 1 12 12
2 3 2 4 8
3 4 6 1 6

最终:

复制代码
res = [24,12,8,6]

整个过程没有使用除法,也没有为每个位置重新遍历数组。

八、Java 完整代码

复制代码
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;

        int[] pre = new int[n];
        pre[0] = 1;

        // 计算前缀积:pre[i] 表示 nums[i] 左侧所有元素的乘积
        for (int i = 1; i < n; i++) {
            pre[i] = pre[i - 1] * nums[i - 1];
        }

        int[] suf = new int[n];
        suf[n - 1] = 1;

        // 计算后缀积:suf[i] 表示 nums[i] 右侧所有元素的乘积
        for (int i = n - 2; i >= 0; i--) {
            suf[i] = suf[i + 1] * nums[i + 1];
        }

        int[] res = new int[n];
        for (int i = 0; i < n; i++) {
            res[i] = pre[i] * suf[i];
        }

        return res;
    }
}

十一、复杂度分析

代码虽然进行了三次循环,但每次最多遍历数组一次,因此总时间复杂度仍然是:

复制代码
O(n)

pre 和 suf 都是长度为 n 的辅助数组,因此额外空间复杂度为:

复制代码
O(n)

输出数组 res 通常不计入额外空间,但两个辅助数组仍然需要计入。

相关推荐
镜子AI1 小时前
教培机构AI推荐系统实战:概念漂移检测算法——为什么“去年有效的推荐今年可能失效“
人工智能·算法
垆边人似月.2 小时前
华为OD算法题:查找众数及中位数
数据结构·算法·leetcode
hetao17338372 小时前
2026-10-08 hetao1733837 的刷题记录
c++·算法
CoderYanger3 小时前
一轮复习——F.动态规划模型总结(入门篇)
java·开发语言·数据结构·算法·leetcode·职场和发展·动态规划
YonyouHRSaaS3 小时前
2026年10-11月AI面试选择指南:对国内主流AI面试系统进行对比,看看哪个更值得选!
人工智能·面试·职场和发展·hr·ai面试
浅念-4 小时前
动态规划专题:斐波那契数列模型 + 网格路径DP(LeetCode例题全解析)
笔记·算法·leetcode·面试·职场和发展·动态规划·推荐算法
落魄大学生之流水线上谋生计4 小时前
接口测试实习指南:从基础方法到面试与现场难题
面试·职场和发展
一只旭宝4 小时前
数据结构与算法复习手册:从写过,到讲清楚
开发语言·数据结构·c++·算法
夏幻灵4 小时前
每日一题:LeetCode 20:有效的括号——栈与哈希映射详解
算法·leetcode·哈希算法
hetao17338375 小时前
2026-10-07 hetao1733837 的刷题记录
c++·算法