当前位置: 首页 > news >正文

LeetCode 238:除自身以外数组的乘积 | 前缀积与后缀积

LeetCode 238:除自身以外数组的乘积 | 前缀积与后缀积

引言

除自身以外数组的乘积(Product of Array Except Self)是 LeetCode 第 238 题,难度为 Medium。题目要求在 O(n) 时间内不使用除法计算每个元素除自身以外所有其他元素的乘积。这道题展示了前缀积与后缀积的巧妙应用,是一种典型的空间换时间和时间换空间的权衡。

问题的核心挑战在于如何在不使用除法的情况下,计算每个位置的结果。如果可以使用除法,只需要一次遍历计算所有元素的乘积,然后对每个位置返回 total_product / nums[i] 即可。但除法可能带来精度问题和溢出问题,因此题目要求不使用除法。前缀积与后缀积的方法巧妙地解决了这个问题。

问题分析

题目描述

给定一个整数数组 nums,返回一个数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余所有元素的乘积。要求不使用除法且时间复杂度为 O(n)。

例如,输入 nums = [1,2,3,4],输出 answer = [24,12,8,6],因为 234=24,134=12,124=12,123=6。

问题特点

这道题的关键挑战是时间复杂度要求 O(n) 和不使用除法的限制。如果允许 O(n²) 的时间,可以对每个位置遍历其他所有元素计算乘积。如果允许除法,可以先计算总乘积再除以每个元素。

前缀积与后缀积的方法通过两次线性遍历就解决了问题。第一次遍历从左到右,计算每个位置左侧所有元素的乘积;第二次遍历从右到左,计算每个位置右侧所有元素的乘积,并与左侧乘积相乘得到最终结果。

直观理解

对于位置 i 的结果,可以看作:nums[0] * nums[1] * ... * nums[i-1] * nums[i+1] * ... * nums[n-1],即左侧所有元素的乘积乘以右侧所有元素的乘积。如果我们能预先计算出每个位置左侧和右侧的乘积,就可以 O(1) 计算每个位置的结果。

前缀积与后缀积方法

方法一:使用额外数组

def productExceptSelf(nums): n = len(nums) left = [1] * n right = [1] * n answer = [1] * n for i in range(1, n): left[i] = left[i - 1] * nums[i - 1] for i in range(n - 2, -1, -1): right[i] = right[i + 1] * nums[i + 1] for i in range(n): answer[i] = left[i] * right[i] return answer

这个方法使用两个额外的数组 left 和 right 分别存储每个位置左侧和右侧的乘积,然后合并得到结果。

方法二:空间优化

def productExceptSelf_optimized(nums): n = len(nums) answer = [1] * n for i in range(1, n): answer[i] = answer[i - 1] * nums[i - 1] product = 1 for i in range(n - 2, -1, -1): product *= nums[i + 1] answer[i] *= product return answer

空间优化版本只使用一个 answer 数组。第一次遍历填充左侧乘积,第二次遍历在原数组上累积右侧乘积,并与 answer 中的左侧乘积相乘。

方法三:双指针

def productExceptSelf_two_pointer(nums): n = len(nums) answer = [1] * n left = right = 1 for i in range(n): if i > 0: left *= nums[i - 1] answer[i] *= left for i in range(n - 1, -1, -1): if i < n - 1: right *= nums[i + 1] answer[i] *= right return answer

双指针方法同时从左到右和从右到左遍历,使用 left 和 right 变量分别累积左右乘积。

算法详解

左侧乘积计算

对于位置 i 的左侧乘积 L[i] = nums[0] * nums[1] * ... * nums[i-1]。这个可以通过递推计算:L[0] = 1,L[i] = L[i-1] * nums[i-1]。

在代码中,我们遍历数组,第一次填充 answer[i] 为 L[i]。

右侧乘积计算

对于位置 i 的右侧乘积 R[i] = nums[i+1] * nums[i+2] * ... * nums[n-1]。同样可以通过递推计算:R[n-1] = 1,R[i] = R[i+1] * nums[i+1]。

在代码中,我们从右向左遍历,使用一个变量累积右侧乘积,并与 answer 中的左侧乘积相乘。

为什么不需要除法

因为我们分别计算了左侧和右侧的乘积,最后将它们相乘就得到了除自身以外所有元素的乘积。这个过程不需要除法,避免了精度问题和溢出问题。

复杂度分析

时间复杂度

时间复杂度为 O(n),因为我们进行了两次线性遍历。每次遍历都是 O(n),总共 O(n)。

空间复杂度

方法一使用了三个额外的数组,空间复杂度为 O(n)。方法二和方法三将空间复杂度降低到 O(1)(除了输入和输出数组)。

代码实现

Python 实现

def productExceptSelf(nums): n = len(nums) answer = [1] * n for i in range(1, n): answer[i] = answer[i - 1] * nums[i - 1] product = 1 for i in range(n - 2, -1, -1): product *= nums[i + 1] answer[i] *= product return answer

Java 实现

public int[] productExceptSelf(int[] nums) { int n = nums.length; int[] answer = new int[n]; answer[0] = 1; for (int i = 1; i < n; i++) { answer[i] = answer[i - 1] * nums[i - 1]; } int product = 1; for (int i = n - 2; i >= 0; i--) { product *= nums[i + 1]; answer[i] *= product; } return answer; }

边界情况处理

空数组

当数组为空时,无法计算有意义的结果,通常返回空数组。代码会正确处理,因为循环不会执行。

单个元素

当数组只有一个元素时,根据定义应该返回 [1](只有自身以外的空乘积)。代码中,answer[0] = 1,product 保持为 1(因为内层循环不执行),返回 [1]。

包含零的情况

当数组包含零时,如果只有一个零,那么除该位置以外的乘积都是零。如果有两个或更多零,除这两个位置以外的乘积不为零但很可能是零。算法正确处理了这些情况,因为乘法天然支持零。

全零情况

当所有元素都是零时,根据定义,除自身以外所有元素的乘积都是零(因为其他元素都是零)。算法正确处理了这种情况。

溢出问题

在 Java 等语言中,乘积可能超出整数范围。LeetCode 的测试用例通常在合理范围内设计,但在某些情况下可能需要使用 long 类型来处理溢出。在 Python 中,整数可以自动扩展,没有这个问题。

测试用例

def test_product_except_self(): assert productExceptSelf([1, 2, 3, 4]) == [24, 12, 8, 6] assert productExceptSelf([1, 2]) == [2, 1] assert productExceptSelf([1]) == [1] assert productExceptSelf([0, 1]) == [1, 0] assert productExceptSelf([0, 0]) == [0, 0] assert productExceptSelf([1, 0, 3]) == [0, 3, 0] assert productExceptSelf([2, 3, 4, 5]) == [60, 40, 30, 24] print("所有测试用例通过!")

扩展问题

不能使用额外空间

如果严格要求 O(1) 额外空间(不包括输出数组),空间优化版本的方法二和三都满足要求,只需要常数个额外变量。

返回乘积的数组而不是数组

如果题目要求返回乘积的数组(如 [2, 3, 4, 5] -> [60, 40, 30, 24]),算法不变,直接返回 answer 数组即可。

乘积很大需要取模

如果乘积很大需要取模(如 10^9 + 7),需要在每次乘法后取模。

实际应用场景

除自身以外数组的乘积问题在现实中有很多应用。在金融领域,可以用来计算投资组合中某个资产之外的其他资产的综合收益率。在信号处理中,可以用来计算频谱分析中某个频率分量之外的能量。在数学中,这种运算与"卷积"和"多项式乘法"有密切关系。

前缀积与后缀积的思想也可以推广到其他类似问题,如前缀最大值、后缀最大值等。

总结

除自身以外数组的乘积问题展示了前缀积与后缀积的巧妙应用。通过分别计算每个位置左侧和右侧的乘积,我们可以 O(n) 时间内解决看似需要除法的问题。

这个问题的关键洞察是:对于位置 i 的结果 = 左侧所有元素的乘积 × 右侧所有元素的乘积。分别计算这两个乘积再相乘,就得到了最终结果。希望通过本文的讲解,读者能够掌握前缀积与后缀积的思想,并将其应用到更多类似问题的解决中。

http://www.jsqmd.com/news/875076/

相关文章:

  • 告别密码!5分钟搞定CentOS 7服务器间的SFTP免密互传(附权限避坑指南)
  • 在国产银河麒麟V10上搞定VMware Workstation 17 Pro,手把手教你从下载到创建第一个虚拟机
  • LeetCode 523:连续的子数组和 | 前缀和同余定理
  • 机器学习评估可信度危机:数据污染、选择性报告与结果误报的深度剖析与应对
  • Win10/Win11频繁蓝屏DPC_WATCHDOG_VIOLATION?别慌,用WinDBG的!dpcwatchdog命令5分钟定位元凶
  • [智能体-41]:智能体识别调用外部工具:原理 + 判定手段 + Python 最简代码示例
  • 对抗性环境下基于分布鲁棒优化的k-次模拦截问题求解
  • 基于树莓派与YOLOv8的铁路道口智能安全系统全栈实践
  • Ubuntu 20.04插上网线没反应?手把手教你搞定RTL8111/8168/8411网卡驱动(附自动加载服务配置)
  • Burp Suite扫描深度配置指南:被动扫描、主动扫描与自定义插入点协同调优
  • 信息论视角下的模型压缩与贝叶斯非参数建模理论边界分析
  • 卷积神经网络频谱分析与LFA-SVD优化方法
  • 当国产欧拉系统遇上VMware ESXi:一次非官方兼容环境的部署实践与思考
  • Pico Neo3 Unity XR开发实战:从黑屏到手柄响应的完整链路
  • LeetCode 724:寻找数组的中心下标 | 前缀和的平衡点
  • [智能体-42]:深度解读:Python 免编译 + 动态执行,支撑智能体落地大模型决策
  • Juno平台TF-A安全调试功能恢复与配置指南
  • 深入解析:浏览器如何“咀嚼”HTML头部——从字节流到渲染树的完整链路与性能优化实战
  • 鸿蒙electron跨端框架PC墨案写作实战:把 Markdown 正文区做成桌面写作的中心
  • LeetCode 1248:统计「优美子数组」 | 前缀和与奇数计数
  • 基于FeFET的动态可重构FPGA:实现亚纳秒级上下文切换的硬件加速新架构
  • 司法AI风险评估:性能与公平性的技术悖论与工程实践
  • 反事实推理:用因果视角评估与缓解AI模型偏见
  • 基于LLM与多智能体的微服务自治运维系统设计与实践
  • 边缘计算融合触觉互联网与数字孪生:构建超低延迟人机交互框架
  • 稀疏结式与动作矩阵:多项式方程组求解的几何代数化方法
  • 鸿蒙electron跨端框架PC片段匣实战:给常用代码片段一个能搜索、复制和整理的桌面仓
  • FPGA加速机器学习在粒子物理触发系统中的应用与实战
  • 计算机视觉模型失败模式自动化发现与自然语言描述技术详解
  • Unity PBR材质工作流:800个开箱即用的工业级材质球