hot100 除了自身以外数组的乘积(238)

本题采用前缀与后缀乘积空间复用算法解决"除自身以外数组的乘积"问题。其核心本质是在禁用除法的前提下,利用输出数组直接存储前缀连续乘积,并引入单个滚动变量逆向累乘后缀,从而将传统多数组方案的额外空间复杂度由 O(n) 降低至 O(1)。该解法实现了时间与额外空间的双重理论极限,最终走向是通过两次完备的线性扫描直接锁定目标乘积序列。

一、 问题本质与数据模型

对于长度为 n 的整数数组 nums,设任意位置 i(其中 0 <= i < n)的目标乘积为 answeri

根据题目定义,answeri 等于该位置左侧所有元素的乘积(前缀乘积)与右侧所有元素的乘积(后缀乘积)的乘积。物理模型可拆解为:

  • Prefixi = nums0 * nums1 * ... * numsi-1 (约定 Prefix0 = 1)

  • Suffixi = numsi+1 * numsi+2 * ... * numsn-1 (约定 Suffixn-1 = 1)

  • answeri = Prefixi * Suffixi

传统方案需要分别开辟两个独立的空间为 O(n) 的辅助数组来存储 Prefix 和 Suffix。而本算法通过重用输出数组并配合动态变量,彻底消除了额外辅助数组的开销。

二、 算法演进对比

在处理区间全量乘积(去中心化)问题时,空间复用双向扫描法达成了资源配置的最优解:

解法名称 时间复杂度 空间复杂度 核心原理 物理瓶颈 / 缺陷
除法扫描法 O(n) O(1) 先计算全局总乘积,遍历时除以当前元素得到结果 无法处理数组中包含元素 0 的特殊情况,且违反题目禁用除法的物理约束
暴力双重循环 O(n^2) O(1) 每到一个位置,线性遍历其余 n-1 个元素并求乘积 存在大量重叠区间的重复计算,大数级样本下必然超时
双辅助数组法 O(n) O(n) 开辟 PREFIX 和 SUFFIX 数组,分别向前和向后累乘后进行对应相乘 引入了两倍于输入规模的额外内存开销
空间复用扫描(当前解法) O(n) O(1) 利用返回数组替代 PREFIX 数组,利用动态单变量滚动代替 SUFFIX 数组 需进行正反两次完备的线性扫描

三; 核心控制流逻辑推导

提供的源码通过解耦前缀和后缀的计算,分两步完成了对输出数组 ans 的构建:

1. 正向扫描:构建前缀乘积

  • 控制循环:for (int i = 1; i < nums.length; i++)

  • 初始边界设定 ans[0] = 1。因为索引 0 左侧没有元素,前缀乘积默认为 1。

  • 状态转移:ans[i] = ans[i - 1] * nums[i - 1]

  • 逻辑证明 :此时 ans[i] 严格等于 nums[0]nums[i-1] 的连续累乘。当这轮循环结束时,ans 数组中已经保存了所有元素对应的完整前缀乘积。

2. 逆向扫描:融合后缀乘积

  • 控制循环:for (int i = nums.length - 1; i >= 0; i--)

  • 引入滚动变量 r,初始值为 1,代表当前位置右侧所有元素的累乘和(即后缀乘积)。

  • 复合更新:ans[i] *= r

  • 此时 ans[i] 的值变为:原有的前缀乘积 * 当前的后缀乘积 r,直接完成了 answer[i] 的最终物理赋值。

  • 动态演进:r *= nums[i]

  • 更新后,r 融入了当前的 nums[i],为左边下一个位置 i-1 的后缀乘积做好了数据准备。

四、 算法执行状态机步进示例

以输入数据 nums = [1, 2, 3, 4] 为例,算法内部各变量与 ans 数组的动态演进过程如下表所示:

阶段 / 步骤 当前指针 i 当前元素 numsi 滚动变量 r 的值 ans 数组实时状态 物理意义说明
初始化 - - - [1, 0, 0, 0] 设定首位前缀初始值
正向第 1 步 1 2 - [1, 1, 0, 0] ans1 = ans0 * nums0 = 1 * 1
正向第 2 步 2 3 - [1, 1, 2, 0] ans2 = ans1 * nums1 = 1 * 2
正向第 3 步 3 4 - [1, 1, 2, 6] ans3 = ans2 * nums2 = 2 * 3 (正向结束)
逆向第 1 步 3 4 1 [1, 1, 2, 6] ans3 *= 1 -> 6; r 更新为 1 * 4 = 4
逆向第 2 步 2 3 4 [1, 1, 8, 6] ans2 *= 4 -> 8; r 更新为 4 * 3 = 12
逆向第 3 步 1 2 12 [1, 12, 8, 6] ans1 *= 12 -> 12; r 更新为 12 * 2 = 24
逆向第 4 步 0 1 24 [24, 12, 8, 6] ans0 *= 24 -> 24; r 更新为 24 * 1 = 24

五、源码实现

复制代码
class Solution {
    public int[] productExceptSelf(int[] nums) {
        // 创建结果数组,根据空间复杂度分析原则,该数组不计入额外空间开销
        int[] ans = new int[nums.length];
        
        // 边界初始化:第一个元素左边没有元素,前缀乘积设定为 1
        ans[0] = 1;
        
        // 步骤 1:正向遍历,计算每个元素的所有前缀乘积
        for (int i = 1; i < nums.length; i++) {
            ans[i] = ans[i - 1] * nums[i - 1];
        }
        
        // 动态滚动变量 r,用来实时维护当前元素右侧所有元素的连续乘积(后缀乘积)
        int r = 1;
        
        // 步骤 2:逆向遍历,将前缀乘积与动态生成的后缀乘积进行融合
        for (int i = nums.length - 1; i >= 0; i--) {
            // 当前位置的最终结果 = 已经存在数组中的前缀乘积 * 当前右侧的后缀乘积
            ans[i] *= r;
            // 滚动变量更新:将当前元素纳入后缀乘积,供左侧的下一个元素使用
            r *= nums[i];
        }
        
        return ans;
    }
}

六、 复杂度极限分析

1. 时间复杂度:O(n)

  • 分析 :算法包含两个独立的、不嵌套的单层 for 循环。第一个循环从前往后遍历长度为 n 的数组,执行次数为 n-1 次;第二个循环从后往前逆序遍历数组,执行次数为 n 次。两轮遍历中的基础操作均为单次的常数阶乘法运算(O(1))。

  • 结论:总体总基本操作次数与输入数据规模 n 呈线性正比关系,时间复杂度为稳定的 O(n)。

2. 空间复杂度:O(1)

  • 分析 :根据题目设定的空间复杂度分析原则,输出数组 ans 作为返回值不被计入额外的物理空间开销。在算法执行期间,除返回值外,仅独立申请了一个基础类型的整型滚动变量 r 以及循环控制变量 i

  • 结论:算法所额外分配的物理内存空间完全恒定,不随输入规模 n 的扩大而产生任何增长,空间复杂度达到了 O(1) 阶的极限原地状态。