力扣刷题#9:LeetCode238除自身以外数组的乘积

238. 除自身以外数组的乘积 | 刷题笔记


题目回顾

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

约束:

  • 不使用除法
  • O(n) 时间复杂度

示例:

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

第一步:先想"如果可以用除法"——找到直觉

刷算法题一个很好的习惯是:先不考虑限制条件,用最直觉的方式想清楚"这个答案应该怎么算"。

[a, b, c, d]

总乘积 = a × b × c × d
answer[0] = b × c × d     ← 总乘积去掉 a
answer[1] = a × c × d     ← 总乘积去掉 b
answer[2] = a × b × d     ← 总乘积去掉 c
answer[3] = a × b × c     ← 总乘积去掉 d

有了这个理解,再看"不能使用除法"这个约束——本质就是不让你先算总乘积再逐个除,逼你换个角度。


第二步:发现"左边 × 右边"的规律

把我手算出来的结果重新排列,换个角度看每个 answer[i]:

answer[0] =        (       b × c × d)  ← 右边三数乘积
answer[1] = (a) ×  (       c × d)      ← 左边 1 个数 × 右边 2 个数
answer[2] = (a × b) ×  (d)             ← 左边 2 个数 × 右边 1 个数
answer[3] = (a × b × c) × ( )          ← 左边 3 个数

观察这个拆分,能明显看出:

每个 answer[i] = 左边所有数的乘积 × 右边所有数的乘积

这就是这道题的核心规律。"前缀积 × 后缀积"——一个在好几种题目中都会反复出现的模式。


第三步:两趟扫描,直观实现

顺着上面发现的规律,最自然的方式就是分两趟:

  1. 从左往右:记录每个位置左边所有元素的乘积
  2. 从右往左:记录每个位置右边所有元素的乘积
  3. 合并:左积 × 右积 = 答案

第一版实现

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        // 用两个额外数组分别存左积和右积
        vector<int> left, right, res;
        int len = nums.size();

        // 先记录每个位置左边的乘积
        for (int i = 0; i < len; i++) {
            if (i == 0) {
                left.push_back(1);
            } else {
                left.push_back(left.back() * nums[i - 1]);
            }
        }

        // 再记录每个位置右边的乘积
        for (int i = len - 1; i >= 0; i--) {
            if (i == len - 1) {
                right.push_back(1);
            } else {
                right.push_back(right.back() * nums[i + 1]);
            }
        }

        // 合并
        for (int i = 0; i < len; i++) {
            res.push_back(left[i] * right[len - 1 - i]);
        }

        return res;
    }
};

当时对着 right 数组的下标想了半天——它和 left 的方向是反的,最后合并时要用 len - 1 - i 来把右边的值对应到正确的位置。


第四步:踩坑与改进

坑 1:把当前元素自己乘进去了

第一次写的时候,left[i] 里存的是"前 i+1 个数的乘积",也就是说包含了 nums[i] 自己。这样后面的合并就全错了,所有位置的答案都一样。

修正方法很简单:先存,再乘,这样 left[i] 永远存的是 i 前面的乘积,不包含 i 自己。

坑 2:下标边界算错了

res[len-1] 应该是最后一个位置(右边没有元素),它的右积应该是 1 而不是某个下标计算出来的值。后来调整了初始化和下标的取法,用实际的例子手算了一遍才跑对。

改进版:用一个循环同时构建左积和右积

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        vector<int> l, r, res;
        int len = nums.size();
        l.push_back(1);
        r.push_back(1);
        for (int i = 1; i < len; i++) {
            l.push_back(l.back() * nums[i - 1]);
            r.push_back(r.back() * nums[len - i]);
        }
        res.push_back(r[len - 1]);
        for (int i = 1; i < len; i++) {
            res.push_back(l[i] * r[len - 1 - i]);
        }
        return res;
    }
};

复杂度: 时间 O(n),空间 O(n)


第五步:进一步优化——省掉额外数组

上一个版本虽然 AC 了,但用了三个额外的 vector。其实可以更进一步:直接在 answer 数组上操作,用两个变量代替两个数组。

从右往左遍历时,用一个 right 变量边走边累乘:

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int n = nums.size();
        vector<int> answer(n);

        // 第一趟:左积存入 answer
        int left = 1;
        for (int i = 0; i < n; i++) {
            answer[i] = left;
            left *= nums[i];
        }

        // 第二趟:从右往左,合并右积
        int right = 1;
        for (int i = n - 1; i >= 0; i--) {
            answer[i] *= right;
            right *= nums[i];
        }

        return answer;
    }
};

复杂度: 时间 O(n),空间 O(1)(在力扣刷题中,返回值不计入额外空间)

这种写法比前面那个直观很多,而且不用操心下标映射的问题。


关键思考总结

1. 这种"两趟扫描"到底在做什么

当题目要求你计算"除了自己以外的信息"时,分两趟是一种通用思路:

  • 第一趟从某个方向收集信息
  • 第二趟从反方向收集信息
  • 最终把两次的信息合并

这个模式在接雨水、柱状图中最大矩形等题目中都会反复出现,熟练了会很有帮助。

2. 向右是从左到右,向右也是从右到左

下标反向计算的时候,可以换一个思路理解:

从左往右是"累乘前面的",从右往左本质上也只是"累乘后面的"——顺着它走的方向来推,不要强行在脑子里反转下标。

用直观的循环方向(从 len-1 往下走到 0)配合一个变量,比手动算 len - 1 - i 容易理解得多。

3. 避免"把当前元素多乘进去"的小技巧

如果在一个循环里既要记录当前位置上方/左边的信息,又要为下一个位置更新累乘值,可以用先存再乘的顺序——这样当前元素就不会污染到本不该包含它的位置。


附录:复杂的下标推导笔记

当时卡最久的地方就是这个反向索引,专门记一笔。

建立 r 数组时的逻辑:

nums = [1, 2, 3, 4]

r[0] = 1            (初始值,还没乘任何元素)
r[1] = 1 × nums[4-1] = 1 × 4    → 4
r[2] = 4 × nums[4-2] = 4 × 3    → 12
r[3] = 12 × nums[4-3] = 12 × 2  → 24

所以 r[k] 的含义是:从右往左数 k 个元素的乘积

合并时 r[len-1-i] 的含义:

位置 i 右边的元素个数 取 r[ ]
i=0 3 个(下标 1,2,3) r[3] 24
i=1 2 个(下标 2,3) r[2] 12
i=2 1 个(下标 3) r[1] 4
i=3 0 个 r[0] 1

右边元素个数 = len - 1 - i,所以取 r[len - 1 - i]


本文档由 AI 辅助生成,作者提供问题和思路,综合获得以上内容。

posted @ 2026-06-07 12:19  STA_running  阅读(10)  评论(0)    收藏  举报