Hot 100 --- 除自身以外数组的乘积

本文概览:本文以LeetCode经典题目"除自身以外数组的乘积"为例,从暴力解法入手,分析不能使用除法导致的重复计算问题,再通过空间换时间的方式用右累计乘积数组优化,将时间复杂度从 O(n²) 优化到 O(n)

一、题目

二、题目分析

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

目标:计算每个位置除自身以外所有元素的乘积,且不能使用除法

核心问题:如果不限制除法,只需要算出总乘积再除以当前元素即可。但题目要求不能使用除法,这就迫使我们换一种方式来计算

思路概览

Java实现代码如下

Java 复制代码
public int[] productExceptSelf(int[] nums) {
    // 1.创建一个结果数组
    int[] res = new int[nums.length];
    // 2.创建一个右边的累计乘积数组
    int[] right = new int[nums.length];
    int rightMul = 1;
    // 初始化右边的累计乘积数组的最后一个元素为1
    right[nums.length - 1] = rightMul;
    // 从右往左遍历,计算右边的累计乘积
    for (int i = nums.length - 2; i >= 0; i--) {
        rightMul *= nums[i + 1];
        right[i] = rightMul;
    }
    // 3.创建一个左边的累计乘积变量
    int leftMul = 1;
    // 4.从左往右遍历,计算结果数组
    for (int i = 0; i < nums.length; i++) {
        res[i] = leftMul * right[i];
        leftMul *= nums[i];
    }
    return res;
}

思路简要说明

  1. 右累计乘积数组right[i] 记录位置 i 右侧所有元素的乘积,从右往左一次遍历预计算

  2. 左累计乘积变量leftMul 记录位置 i 左侧所有元素的乘积,从左往右遍历时累乘即可,不需要额外数组

  3. 结果计算res[i] = leftMul * right[i],左侧乘积和右侧乘积相乘就是除自身以外的乘积

三、思路详解

暴力解法入手

最自然的想法是:如果可以使用除法,只需要算出所有元素的总乘积,然后对每个位置除以 nums[i] 就能得到结果,时间复杂度 O(n)

但题目要求不能使用除法,那最直接的做法就是:每遍历到一个位置,分别累乘它左边的所有元素和右边的所有元素,然后相乘

如果可以使用除法,右边的累计乘积只需要算一次,之后每次除以当前遍历到的值就行了。但不能使用除法,就导致每次都要重新累乘右边的所有元素,而右边每次其实只变化了一个乘数,却要重复计算整个右边的乘积

  • 时间复杂度:O(n²),对每个位置都要累乘左右两侧
  • 核心瓶颈:右边的累计乘积每次只变化了一个乘数,却要重复计算整个乘积,做了大量无效计算
  • 关键思考:能否把右边的累计乘积也像左边一样,用某种方式存储下来,避免重复计算?

左右累计乘积解法

思路分析

暴力解法的问题在于:左边的乘积可以从左往右累乘,每次只多乘一个数,不需要重复计算;但右边的乘积每次都要重新算,因为不能用除法把上一次的结果"除掉"一个数

那最简单而且自然的思路就是空间换时间 :创建一个数组 right[],用来记录每个位置对应的右边的累计乘积。只要从右往左遍历一次,就能得出这个数组。然后再对原数组从左往右遍历一次,左边的乘积用变量累乘,右边的乘积直接从 right[] 数组中 O(1) 读取

具体步骤

  1. 预计算右累计乘积数组 :从右往左遍历,right[i] = nums[i+1] × nums[i+2] × ... × nums[n-1],即位置 i 右侧所有元素的乘积。用累乘的方式,rightMul *= nums[i+1],每次只多乘一个数

  2. 计算结果数组 :从左往右遍历,leftMul 累乘左侧元素,res[i] = leftMul * right[i],然后 leftMul *= nums[i]

为什么左边用变量、右边用数组?

因为我们的遍历方向是从左往右,左边的乘积可以随着遍历逐步累乘,不需要存储历史值;而右边的乘积需要提前算好,因为从左往右遍历时,右边的乘积是"未来"的值,还没算到,所以必须预先存储

举例说明

nums = [1, 2, 3, 4] 为例

第一步:计算 right 数组

从右往左遍历:

i numsi+1 rightMul righti
3 - 1 right3 = 1
2 4 1×4=4 right2 = 4
1 3 4×3=12 right1 = 12
0 2 12×2=24 right0 = 24

right = [24, 12, 4, 1]

第二步:计算结果数组

从左往右遍历:

i leftMul righti resi = leftMul × righti leftMul 更新
0 1 24 1×24=24 1×1=1
1 1 12 1×12=12 1×2=2
2 2 4 2×4=8 2×3=6
3 6 1 6×1=6 6×4=24

最终结果为 [24, 12, 8, 6]

验证:2×3×4=24, 1×3×4=12, 1×2×4=8, 1×2×3=6,正确

  • 时间复杂度:O(n),两次遍历
  • 空间复杂度 :O(n),right 数组占用额外空间(结果数组 res 不算额外空间)
相关推荐
Frank学习路上1 小时前
【C++】面试:STL容器与算法
c++·算法·面试
10岁的博客1 小时前
NOIP2010普及组「接水问题」详解:模拟算法与优先队列解法
开发语言·c++·算法
彼岸星光ぐ>1 小时前
排序算法对比
数据结构·算法·排序算法
Sam09271 小时前
Java 转 AI Agent 开发:Java 和 Python 的区别与快速学习指南
java·人工智能·python·ai
heimeiyingwang1 小时前
【架构实战】数据脱敏与隐私保护:合规是底线
java·开发语言·架构
dengyuezhe80602 小时前
《C++ 异常机制与智能指针:从原理到实现》
android·java·c++
于指尖飞舞2 小时前
java后端面试题(常用集合极简)
java·开发语言·面试
YHHLAI2 小时前
LeetCode 1.两数之和 | 从暴力枚举到线性优化
算法·leetcode·职场和发展