LeetCode 238:除自身以外数组的乘积(前缀和) —— 题解

👋 欢迎阅读

🏠个人主页: 愿旖旎
📘专栏传送门: 算法专栏
💻当前学习内容:前缀和

🎯 欢迎来到「除自身以外数组的乘积」题解之旅! 本文将带你从"算出每个位置除自己外其他数的乘积"这一直观场景出发,深入理解前缀积 + 后缀积 的巧妙运用,并掌握如何用两次累乘预处理左右乘积在 O(n) 内得到全部答案

在开始之前,建议你先:

  • 了解题目背景 :这是 LeetCode 238 题,给定整数数组 nums,返回数组 answer,其中 answer[i] 等于 numsnums[i] 之外 所有元素的乘积,且不允许使用除法 。本质上,每个答案 = 左侧所有数的乘积 × 右侧所有数的乘积 ,问题转化为预处理前缀积与后缀积再相乘

  • 明确学习目标 :掌握前缀积 f 与后缀积 g 的构建 ,理解 fi、gi 均不含 numsi 本身 的语义与空积初始化为 1 ,并熟练处理含 0 元素单元素数组等边界情况。

  • 准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 nums = [1,2,3,4] 输出 [24,12,8,6])。

本文将从问题转化、左右累乘、双积相乘、边界防护 到代码实现,层层递进。即使你对前缀积 还不熟悉,我们也会从"左边的积乘上右边的积,就是除自己外的积"这一直觉出发,让你轻松抓住核心思想 ------左积右积,相乘即答。现在,让我们一起累乘左右两侧,算出每个位置的乘积吧! ✖️🎯

一.题目

238. 除了自身以外数组的乘积 - 力扣(LeetCode)

二、算法分析

一、问题分析(前置分析)

  • 题目要求:返回数组 answeranswer[i] = numsnums[i] 所有元素的乘积,禁止使用除法
  • 关键约束:不能用除法 ;元素可能为 0(除法会失效,本解法天然规避);要求 O(n) 时间。
  • 核心思路:暴力做法对每个位置都重新乘一遍其他元素 ,总代价 O(n²);用前缀积 f (左侧乘积)与后缀积 g (右侧乘积)各预处理一遍,answer[i] = f[i] * g[i],总复杂度 O(n)

📌 例子:暴力为什么不可行(且除法为何不能用)

数组 [1, 2, 3, 4]answer[0] 需算 2×3×4answer[1] 又要算 1×3×4------每个位置的乘积都从头重乘 ,重复计算大量重叠区间,最坏 O(n²)。而用除法 总积 / nums[i] 看似 O(n),但 nums0 时(如 [1,0,3,4])总积为 0,0/0 无法处理------这正是题目禁止除法的原因,前缀积方案天然规避。

二、算法策略(前缀积 f + 后缀积 g)

核心步骤:

  1. 初始化f[0] = 1(下标 0 左侧无元素,空积为 1 )、g[n-1] = 1(下标 n-1 右侧空积为 1)。
  2. 构建前缀积 ff[i] = f[i-1] * nums[i-1],表示下标 i 左侧所有元素的乘积(从左往右)。
  3. 构建后缀积 gg[j] = g[j+1] * nums[j+1],表示下标 j 右侧所有元素的乘积(从右往左)。
  4. 相乘得到答案answer[i] = f[i] * g[i](左侧积 × 右侧积,恰好不含自身)。

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

下标 i 0 1 2 3
numsi 1 2 3 4
fi(左侧积) 1 1 2 6
gi(右侧积) 24 12 4 1
answeri = fi×gi 24 12 8 6

构建时 f 从左往右逐个累乘f[3] = f[2] × nums[2] = 2 × 3 = 6,即 1×2×3),g 从右往左递推(g[0] = g[1] × nums[1] = 12 × 2 = 24,即 2×3×4);answer[3] = f[3] × g[3] = 6 × 1 = 6(= 1×2×3),每个答案恰好是除自身外全部元素的积。

三、正确性说明(简单版本)

  • f、g 语义精确f[i] = nums[0] × ... × nums[i-1] 递推保证恰好是 i 左侧全部元素的乘积,不含 numsig[i] 对称地是右侧全部元素的乘积,递推无误差。
  • 相乘条件等价answer[i] = f[i] × g[i] = 左侧积 × 右侧积 = 除 numsi 外所有元素的乘积 ,与题目定义逐字对应,不会算错
  • 空积单位元正确f[0] = 1g[n-1] = 1 使"空侧"的乘积等于乘法单位元 1,乘上 1 不影响结果,首尾位置也能正确得到答案。
  • 含 0 时仍成立 :乘法中 0 的传播是精确的,f、g 中 0 出现的位置由实际乘积决定,不依赖除法,任何含 0 的输入都正确。

📌 例子:为什么 fi 不含 numsi 本身

下标 3 处:f[3] = 1 × 2 × 3 = 6g[3] = 1(空积),answer[3] = 6 × 1 = 6------nums[3] = 4 没有参与任何一侧的乘积 ;若误把自身乘进去(如 f[3] 写成 1×2×3×4),答案会多乘一个 4,全部错位。

四、实现细节(边界防护)

  • 初始化:fg 均开 n 大小;f[0] = 1g[n-1] = 1乘法单位元,不是 0!)。
  • 边界防护:构建 f 时 i 从 1 到 n-1,构建 g 时 jn-2 到 0,均不越界n = 1f[0] = g[0] = 1answer[0] = 1(空乘积,正确);元素含 0 时 f、g 正常传播 0,无需特判
  • 复杂度:时间 O(n) (两次构建 + 一次相乘),空间 O(n)(两个辅助数组;可优化到 O(1),见难点5)。
  • 关键操作f[i] = f[i-1] * nums[i-1](前缀累乘)、g[j] = g[j+1] * nums[j+1](后缀累乘)、answer[i] = f[i] * g[i](合并答案)。

📌 例子:为什么空积必须是 1 而不是 0

nums = [1, 2, 3, 4]f[0] = 1(下标 0 左侧没有元素)。若误初始化为 0,f[1] = 0 × 1 = 0,后续全部前缀积都是 0 ,答案整体错误;空积取单位元 1 ,乘上它不影响后续累乘,首尾答案才正确------这是前缀积与前缀和(空和为 0)的本质区别

五、返回值(目标映射)

  • 返回 v除自身以外数组的乘积v[i] = f[i] * g[i],对应题目"返回数组 answer,其中 answeri 等于 nums 中除 numsi 之外所有元素的乘积"。

三.代码

cpp 复制代码
class Solution
{
public:
    vector<int> productExceptSelf(vector<int>& nums)
    {
        int n = nums.size();
        vector<int> v;          // 结果数组
        vector<int> f(n);       // 前缀积:f[i] = 下标 i 左侧所有元素的乘积
        vector<int> g(n);       // 后缀积:g[i] = 下标 i 右侧所有元素的乘积

        f[0] = 1;               // 空积:左侧无元素,乘积为单位元 1
        g[n - 1] = 1;           // 空积:右侧无元素,乘积为单位元 1

        // 1. 构建前缀积 f:从左往右累乘(不含 nums[i] 自身)
        for (int i = 1; i < n; i++)
        {
            f[i] = f[i - 1] * nums[i - 1];
        }

        // 2. 构建后缀积 g:从右往左累乘(不含 nums[j] 自身)
        for (int i = n - 2; i >= 0; i--)
        {
            g[i] = g[i + 1] * nums[i + 1];
        }

        // 3. 合并答案:左侧积 × 右侧积 = 除自身外全部元素的乘积
        for (int i = 0; i < n; i++)
        {
            v.push_back(f[i] * g[i]);
        }
        return v;
    }
};

四、易错点分析

难点1:fi、gi 的语义------"不含 numsi 本身"

cpp 复制代码
f[i] = f[i - 1] * nums[i - 1];   // 乘的是 nums[i-1],不是 nums[i]
g[i] = g[i + 1] * nums[i + 1];   // 乘的是 nums[i+1],不是 nums[i]

fi 表示 i 左侧 的积,递推乘的是 nums[i-1];gi 表示 i 右侧 的积,乘的是 nums[i+1]最容易写错的是下标偏移 :若写成 f[i] = f[i-1] * nums[i],自身被乘进前缀积,所有答案多乘一个自身,结果完全错误。

难点2:空积必须初始化为 1,而不是 0

cpp 复制代码
f[0] = 1;        // 乘法单位元
g[n - 1] = 1;

前缀和"空和为 0"的直觉不能照搬 到前缀积:0 是加法的单位元,但乘法的单位元是 1 。若 f[0] 初始化为 0,f[1] = 0 × nums[0] = 0所有前缀积全部变成 0 ,答案整体错误。单位元选错是前缀积最隐蔽的错误,且编译器不会报错。

难点3:两个数组的构建方向相反

cpp 复制代码
for (int i = 1; i < n; i++)         // f:从左往右(依赖 f[i-1])
for (int i = n - 2; i >= 0; i--)    // g:从右往左(依赖 g[i+1])

f 依赖前一个 已算好的 fi-1,必须正向;g 依赖后一个 gi+1,必须反向。方向写反 会访问未初始化的元素(越界或垃圾值),结果随机且编译器不报错------这是与前缀和系列完全相同的陷阱。

难点4:为什么不能直接用除法

cpp 复制代码
// 错误示范:answer[i] = 总积 / nums[i]
int total = 1;
for (int x : nums) total *= x;
for (int i = 0; i < n; i++) answer[i] = total / nums[i];

除法看似 O(n) 简洁,但有两个致命问题:① 题目明确禁止 使用除法;② nums0 时(如 [1,0,3,4]),总积为 0,0 / 0 对 i=1 产生未定义行为 ,且多个 0 时除法逻辑彻底失效。前缀积方案不依赖除法,天然规避 0 问题

五、流程图

🎯 闭幕

🎉 恭喜你完成了「除自身以外数组的乘积」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 本题使用 前缀积 f[i]后缀积 g[i] ,分别记录每个位置左侧和右侧所有元素的乘积。为什么 f[0] = 1g[n-1] = 1 这里的单位元 1 在乘法运算中起到什么作用?

  • 构建 f 数组时,f[i] = f[i-1] * nums[i-1]为什么左侧积不包含 nums[i] 自身? 如果包含自身,最终的答案公式应如何调整?

  • 本题使用了 两个辅助数组 ,空间复杂度 O(n)。能否只用一个结果数组 ans 和两个变量(或者一个变量)来实现 O(1) 额外空间? 请简述思路。

  • 如果数组中存在 0 ,当前算法是否仍然正确?请举例说明(如 nums = [0, 1, 2])。

  • 如果题目要求 返回结果数组的同时,不能使用除法运算,当前方法满足要求吗?相比使用除法,这种方法有什么优势?

👍 点赞 / 收藏
👤 关注作者 ,获取更多题解
💬 留言交流你的疑问或优化思路


📌 深入思考答案

  • f[0]=1g[n-1]=1 表示 空积(没有元素时的乘积),在乘法中单位元为 1,这样即使中心下标在端点,左侧或右侧为空时乘积仍为 1,与其他位置的乘积能正确相乘。

  • 不包含自身 是为了直接计算"除自身外"的乘积;若包含自身,则最终答案需除以 nums[i],但这会引入除法且无法处理 0。

  • 可优化为 O(1) 额外空间 :先用 ans 数组存储左侧积,再从右向左扫描,用变量 rightProd 累乘右侧元素,同时 ans[i] *= rightProd,最后返回 ans,无需额外数组。

  • 含 0 时依然正确 :例如 [0,1,2]f=[1,0,0]g=[2,2,1],结果 [2,0,0],符合"除自身外乘积"(第1个为 1*2=2,第2个为 0*2=0,第3个为 0*1=0)。

  • 不使用除法 正是本题亮点,避免了除零和精度问题,且对所有整数(包括 0)通用。

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

相关推荐
一条大祥脚19 分钟前
26杭电暑期第八场(后半)快读|快写|tarjan|路径DP|mex转化|扫描线|前缀和|二分图
数据结构·算法·tarjan·杭电多校·强联通分量·动态规划dp
奈斯先生Vector22 分钟前
从“能调用”到“可运营”:AI Agent 进入多模型时代后的架构升级
java·javascript·数据库·人工智能·算法·架构·aigc
Escalating_xu23 分钟前
【C++类和对象(上)】从类的定义、封装与对象模型到内存对齐和 this 指针
开发语言·前端·c++
吃着火锅x唱着歌27 分钟前
Effective C++ 学习笔记 条款45 运用成员函数模板接受所有兼容类型
c++·笔记·学习
2601_9561219734 分钟前
Bellman-ford算法
算法
爱和冰阔落38 分钟前
【Linux】多线程打印为什么会乱?pthread 创建、等待、退出、取消与分离全实战
linux·运维·c++·redis
叠层归一研究院40 分钟前
基于区分本源的此刻存在唯一性本体论推演
人工智能·算法
luj_176841 分钟前
尾椎藏今生记忆?骨盆对应不确定性
开发语言·网络·c++·经验分享·算法