前缀积与后缀积 —— LeetCode 238. 除了自身以外数组的乘积

题目描述(题目链接):

给定一个长度为 n的整数数组 nums,要求返回一个数组 answer,其中 answeri 等于 nums 中除了 numsi 之外其余各元素的乘积。

严苛的限制条件:

  1. 禁止使用除法:不能先求出总乘积再除以当前元素。
  2. 时间复杂度O(n):不能使用嵌套循环暴力求解。
  3. 进阶空间要求 O(1):除了输出数组外,不能申请额外的与 n相关的空间。

面对这些限制,传统的暴力解法(双重循环)和除法解法(需处理 0 和精度问题)均失效。我们需要一种更巧妙的前缀与后缀思想。

核心算法思想:前缀积 × 后缀积

对于数组中的任意一个位置 i,我们要求的 answeri 实际上可以拆解为两部分:

即:当前位置左侧所有元素的乘积(前缀积) 乘以 当前位置右侧所有元素的乘积(后缀积)。

算法设计步骤

为了满足 O(1)的额外空间复杂度(不计算输出数组),我们可以复用输出数组 answer 来存储中间结果,并通过两次遍历完成计算:

  1. 第一次遍历(从左到右) :
    计算每个位置 i 的左侧乘积 ,并直接存入 answeri 中。此时 answeri 表示 numsi 左边所有元素的乘积。
    • answer0 = 1(第一个元素左边无元素,乘积为 1)。
    • answeri = answeri-1 * numsi-1。
  2. 第二次遍历(从右到左) :
    使用一个变量 R 动态维护当前位置右侧所有元素的乘积 (初始 R = 1)。在遍历过程中,将 R 乘入 answeri,然后更新 R。
    • answeri = answeri * R(此时 answeri 变为左侧乘积 × 右侧乘积)。
  • R = R * numsi(更新 R,为下一个左侧元素做准备)。

代码实现 (C++)

以下是符合题目进阶空间要求的最优 C++ 实现:

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

        // 第一步:计算左侧乘积并存入 answer
        // answer[i] 表示 nums[i] 左侧所有元素的乘积
        answer[0] = 1; 
        for (int i = 1; i < n; i++) {
            answer[i] = answer[i - 1] * nums[i - 1];
        }

        // 第二步:计算右侧乘积并直接与左侧乘积相乘
        int R = 1; // R 动态维护右侧元素的乘积
        for (int i = n - 1; i >= 0; i--) {
            answer[i] = answer[i] * R; // 左侧乘积 * 右侧乘积
            R = R * nums[i];           // 更新 R,供下一个(左侧)元素使用
        }

        return answer;
    }
};

算法复杂度分析

时间复杂度: O(n)

我们进行了两次遍历,每次遍历处理 n 个元素,总操作数为 2n,渐进时间复杂度为线性阶 O(n)。

空间复杂度: O(1)

除了必须返回的 answer 数组外,我们只使用了常数个变量(R 和循环索引 i),没有开辟额外的数组空间。这完美满足了题目进阶的 O(1)空间复杂度要求。

示例推演

以 nums = 1, 2, 3, 4 为例:

第一次遍历(计算左侧积):

i=0: answer0 = 1

i=1: answer1 = 1 * 1 = 1

i=2: answer2 = 1 * 2 = 2

i=3: answer3 = 2 * 3 = 6

此时 answer = 1, 1, 2, 6

第二次遍历(从右向左乘入右侧积):

初始 R = 1

i=3: answer3 = 6 * 1 = 6;R = 1 * 4 = 4

i=2: answer2 = 2 * 4 = 8;R = 4 * 3 = 12

i=1: answer1 = 1 * 12 = 12;R = 12 * 2 = 24

i=0: answer0 = 1 * 24 = 24;R = 24 * 1 = 24

最终输出: 24, 12, 8, 6,与预期结果完全一致。

相关推荐
All for pursuit.1 小时前
【动态规划-1】121.买卖股票的最佳时机
数据结构·c++·算法·leetcode·动态规划
szial1 小时前
JavaScript 的 call、apply 和 bind:如何控制函数调用
开发语言·前端·javascript
水饺编程1 小时前
第1章,[Win32 章节]:API 及 内存管理模式
c语言·c++·windows·visual studio
weixin_307779131 小时前
C++代码实现MATLAB中的cvpartition函数功能
开发语言·c++·算法·matlab
黑妹天下第一乖1 小时前
小智改造实战解读-首 token 延迟去哪了:云端与端侧大模型的分段对照
开发语言·人工智能·python·嵌入式硬件·自然语言处理·iot
。小二1 小时前
Go 泛型并发利刃:async 库深度评测——370K ops/s、零依赖、生产就绪
开发语言·javascript·golang
(Charon)1 小时前
【C++面试】线程同步机制:互斥锁、条件变量、原子操作与读写锁
开发语言·c++·算法·面试
CV工程师丁Sir2 小时前
ArkWeb 手记 06|window.open 与页面跳转拦截
开发语言·php·harmonyos
夜不会漫长2 小时前
C++:内存管理
java·开发语言·c++