【LeetCode Hot100 刷题日记(16/100)】238. 除自身以外数组的乘积——数组、前后缀分解、双指针、原地操作 🎯

60 阅读6分钟

🎯 题目链接:leetcode.cn/problems/product-of-array-except-self/

🔍 难度:中等 | 🏷️ 标签:数组、前后缀分解、双指针、原地操作

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)(不计输出数组)


🧠 题目分析

给你一个整数数组 nums,返回一个新数组 answer,其中 answer[i] 等于 numsnums[i] 之外其余所有元素的乘积

❌ 不允许使用除法
⏱️ 要求时间复杂度为 O(n)
💾 进阶要求:空间复杂度 O(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
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]

解释:
- index 0: 1*0*(-3)*3 = 0
- index 1: (-1)*0*(-3)*3 = 0
- index 2: (-1)*1*(-3)*3 = 9
- index 3: (-1)*1*0*3 = 0
- index 4: (-1)*1*0*(-3) = 0

⚠️ 关键点:不能用除法! 否则遇到 0 就会出问题。比如 nums = [0, 1, 2],如果用总乘积 / nums[i],那 i=0 时就会出现除以零错误。


🧩 核心算法及代码讲解

✅ 核心思想:前后缀分解(Prefix & Suffix Decomposition)

定义

  • 前缀乘积(Left Product)pre[i] 表示从 nums[0]nums[i-1] 的乘积。
  • 后缀乘积(Right Product)suf[i] 表示从 nums[i+1]nums[n-1] 的乘积。

则有:

answer[i] = pre[i] * suf[i]

💡 为什么这个方法有效?

因为我们想计算“除了自己之外的所有数的乘积”,而这些数可以分为两部分:

  • 左边的所有数(前缀)
  • 右边的所有数(后缀)

所以只需分别预处理左右两边的乘积即可。


🔄 优化思路:空间压缩 → O(1)

虽然我们能用两个数组 LR 分别存储前缀和后缀乘积,但这样空间是 O(n)。

👉 进阶技巧:我们可以 先用输出数组 answer 存储前缀乘积,然后在第二次遍历中动态维护一个变量 R 来表示当前右边的乘积。

✅ 此时:

  • answer[i] 先存的是左边乘积(即 pre[i]
  • 再乘上 R(右边乘积),得到最终结果
  • 每次更新 R *= nums[i],保证下一次迭代时 R 是新的右乘积

📌 关键洞察:输出数组不算额外空间 → 可复用!


🧠 解题思路(分步详解)

  1. 初始化输出数组 answer,用于存储前缀乘积(第一遍)

    • answer[0] = 1,因为第一个元素左边没有数
  2. 第一次遍历(从左到右)

    • 计算每个位置的左侧乘积
    • answer[i] = answer[i-1] * nums[i-1]
    • 即:answer[i]nums[0] ~ nums[i-1] 的乘积
  3. 第二次遍历(从右到左)

    • 维护一个变量 R,初始为 1(代表最右边没有元素)
    • 对每个 i
      • answer[i] = answer[i] * R(左边 × 右边)
      • 更新 R = R * nums[i],为下一个位置准备右边乘积
  4. 返回 answer

✅ 时间复杂度:O(n),两次遍历
✅ 空间复杂度:O(1),仅使用常量级变量(输出数组不计)


📈 算法分析

指标说明
时间复杂度O(n)两次线性扫描
空间复杂度O(1)输出数组不计,仅用一个变量 R
是否可扩展可推广至任意“排除某个位置”类问题
面试价值⭐⭐⭐⭐⭐经典题,考察思维转换能力

🧪 代码实现(完整模板)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 核心函数:除自身以外数组的乘积 —— 前后缀分解 + 原地优化
vector<int> productExceptSelf(vector<int>& nums) {
    int length = nums.size();
    vector<int> answer(length);

    // 第一步:计算前缀乘积,存入 answer 数组
    // answer[i] 表示 i 左侧所有元素的乘积
    answer[0] = 1;  // 第一个元素左边无元素,乘积为 1
    for (int i = 1; i < length; i++) {
        answer[i] = nums[i - 1] * answer[i - 1];  // 当前前缀 = 上一个前缀 × 上一个元素
    }

    // 第二步:从右往左遍历,同时维护右侧乘积 R
    int R = 1;  // R 表示当前位置右边所有元素的乘积
    for (int i = length - 1; i >= 0; i--) {
        // 当前答案 = 左侧乘积 × 右侧乘积
        answer[i] = answer[i] * R;
        // 更新 R:将当前元素加入右侧乘积,供下一次使用
        R *= nums[i];
    }

    return answer;
}

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 测试用例 1
    vector<int> nums1 = {1, 2, 3, 4};
    auto result1 = productExceptSelf(nums1);
    cout << "Test 1: ";
    for (int x : result1) cout << x << " ";
    cout << endl;

    // 测试用例 2
    vector<int> nums2 = {-1, 1, 0, -3, 3};
    auto result2 = productExceptSelf(nums2);
    cout << "Test 2: ";
    for (int x : result2) cout << x << " ";
    cout << endl;

    return 0;
}

🔍 代码逐行解析(核心逻辑)

answer[0] = 1;  // 左边界:第一个元素左边无元素,乘积为 1
for (int i = 1; i < length; i++) {
    answer[i] = nums[i - 1] * answer[i - 1];  // 累积左侧乘积
}

➡️ 比如 nums = [1,2,3,4],执行后:

  • answer = [1, 1, 2, 6]
  • 分别是:1, 1, 1×2, 1×2×3
int R = 1;
for (int i = length - 1; i >= 0; i--) {
    answer[i] = answer[i] * R;  // 左侧 × 右侧
    R *= nums[i];               // 更新右侧乘积
}

➡️ 从右到左:

  • i=3: answer[3] = 6 * 1 = 6, R = 1 * 4 = 4
  • i=2: answer[2] = 2 * 4 = 8, R = 4 * 3 = 12
  • i=1: answer[1] = 1 * 12 = 12, R = 12 * 2 = 24
  • i=0: answer[0] = 1 * 24 = 24, R = 24 * 1 = 24

最终:answer = [24, 12, 8, 6]


💡 面试加分点

✅ 为什么不用除法?

  • 如果数组中有多个 0,则总乘积为 0,无法通过 total / nums[i] 得到正确结果
  • 例如 [0, 0, 1]total = 0answer[0] = 0/0 错误!

✅ 如何处理多个 0?

本题不需要显式处理,因为:

  • 如果只有一个 0,那么只有那个位置的答案非零(等于其他数乘积)
  • 如果有两个或以上 0,则所有位置答案都为 0

📌 所以我们不需要特殊判断,前后缀分解天然支持这种情况

✅ 前后缀分解的通用性

这种模式适用于很多“排除某位置”的问题,例如:

问题思路
除自身外的乘积左右乘积相乘
最大子数组乘积类似动态规划,但需考虑正负
滑动窗口最大值单调队列
数组中每个元素的下一个更大元素单调栈

✅ 掌握“前后缀分解”是解决这类问题的关键!


🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第17题 —— 41.缺失的第一个正数(困难)

🔹 题目:给定一个未排序整数数组,找出其中最小的缺失正整数。

🔹 核心思路:利用数组本身作为哈希表,通过索引映射进行标记。

🔹 考点:原地修改、索引映射、数学技巧、空间优化。

🔹 难度:困难,但思想极为巧妙,是“原地哈希”的经典应用!

💡 提示:不要用额外哈希表!目标是 O(1) 空间 + O(n) 时间!

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!


🎯 坚持每日一题,算法不再难!
📚 专栏持续更新中…