"神奇!不用除法,如何算出数组中每个元素'邻居'的乘积?"

一道看似简单却暗藏玄机的题目

给你一个数组 nums,要求返回一个新数组,其中每个位置的值是原数组除自身外所有元素的乘积
但有两个苛刻条件:

1️⃣ 禁止使用除法 (不能用总乘积除以当前元素)

2️⃣ 必须在 O(n) 时间复杂度内完成

示例1:

ini 复制代码
输入: nums = [1,2,3,4]
输出: [24,12,8,6]

示例2:

ini 复制代码
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]

初步思考

最直观的解法可能是先计算所有元素的乘积,然后对于每个元素,用总乘积除以当前元素。但是这种方法有两个问题:

  1. 题目明确要求不能使用除法
  2. 当数组中有0时,这种方法会失效(除以0是未定义操作)

因此,我们需要寻找一种更聪明的方法。

高效解法:左右乘积列表

算法思路

我们可以利用两个额外的数组 leftright 来分别存储每个元素左侧和右侧的乘积:

  1. left[i] 表示 nums[i] 左侧所有元素的乘积
  2. right[i] 表示 nums[i] 右侧所有元素的乘积
  3. 最终结果 answer[i] = left[i] * right[i]

这样,我们就能在不使用除法的情况下得到每个位置除自身外的乘积。

优化空间复杂度

上面的方法需要 O(n) 的额外空间来存储 leftright 数组。我们可以进一步优化,只使用输出数组和一个变量来存储临时的乘积:

  1. 首先,使用输出数组 res 存储每个元素的左侧乘积
  2. 然后,用一个变量 right 从右到左遍历,计算右侧乘积并乘到 res

JavaScript实现

javascript 复制代码
var productExceptSelf = function(nums) {
    let res = [];
    let left = 1;
    let right = 1;
    
    // 从左到右遍历,计算左侧乘积
    for (let i = 0; i < nums.length; i++) {
        res[i] = left;
        left *= nums[i];
    }
    
    // 从右到左遍历,计算右侧乘积并相乘
    for (let j = nums.length - 1; j >= 0; j--) {
        res[j] *= right;
        right *= nums[j];
    }
    
    return res;
};

算法分析

  • 时间复杂度:O(n),我们只进行了两次线性遍历
  • 空间复杂度:O(1)(不考虑输出数组的空间),因为我们只使用了常数个额外变量

示例解析

让我们用第一个示例 nums = [1,2,3,4] 来逐步分析算法:

第一次遍历(从左到右):

  • i=0: res[0] = 1, left = 1*1 = 1
  • i=1: res[1] = 1, left = 1*2 = 2
  • i=2: res[2] = 2, left = 2*3 = 6
  • i=3: res[3] = 6, left = 6*4 = 24

此时 res = [1, 1, 2, 6]

第二次遍历(从右到左):

  • j=3: res[3] = 61 = 6, right = 14 = 4
  • j=2: res[2] = 24 = 8, right = 43 = 12
  • j=1: res[1] = 112 = 12, right = 122 = 24
  • j=0: res[0] = 124 = 24, right = 241 = 24

最终 res = [24, 12, 8, 6],与示例输出一致。

图示化:

边界情况考虑

  1. 数组中有0的情况

    • 如示例2 nums = [-1,1,0,-3,3]
    • 算法仍然有效,因为我们是分别计算左侧和右侧的乘积,不会出现除以0的问题
  2. 数组长度为1的情况

    • 根据题意,这种情况应该返回 [1](因为可以看作空乘积为1)
    • 我们的算法也能正确处理这种情况
  3. 大数情况

    • 题目保证结果在32位整数范围内,所以我们不需要考虑溢出问题

为什么这种方法有效?

这种方法的巧妙之处在于它将问题分解为两个部分:

  1. 左侧乘积:对于每个元素,计算它左边所有元素的乘积
  2. 右侧乘积:对于每个元素,计算它右边所有元素的乘积

然后将这两部分相乘,就得到了除自身外所有元素的乘积。通过两次遍历(一次从左到右,一次从右到左),我们高效地完成了这个计算。

总结

这道题目展示了如何通过巧妙的数组遍历技巧,在不使用除法的情况下高效解决问题。关键在于:

  1. 将问题分解为左侧乘积和右侧乘积
  2. 利用两次遍历分别计算这两部分
  3. 优化空间复杂度,只使用必要的额外空间

掌握这种"分解问题"和"空间优化"的思想,对于解决许多数组相关的问题都非常有帮助。

希望这篇解析能帮助你更好地理解这个问题及其解法!

相关推荐
SimonKing几秒前
惊!未实现Serializable竟让第三方接口回调全军覆没
前端·程序员·架构
凯哥19703 分钟前
如何将你写的 js 模块发布到 npmjs 给大家使用
前端
章若楠圈外男友8 分钟前
修改了Element UI中组件的样式,打包后样式丢失
前端·vue.js
XU磊26013 分钟前
深入理解表单---提交用户与网页交互的重要方式:GET 与 POST 的本质区别与应用实践
服务器·前端·javascript
爱分享的程序员15 分钟前
前端跨端框架的开发以及IOS和安卓的开发流程和打包上架的详细流程
android·前端·ios
kadog25 分钟前
《Python3网络爬虫开发实战(第二版)》配套案例 spa6
开发语言·javascript·爬虫·python
珎珎啊26 分钟前
uniapp+vue3移动端实现输入验证码
前端·javascript·uni-app
HtwHUAT1 小时前
五、web自动化测试01
前端·css·chrome·python·功能测试·selenium·html
86Eric1 小时前
Vue 中 使用 Mixins 解决 多页面共用相同组件的相关问题
前端·javascript·vue.js·mixins·公用组件
qq_25249639961 小时前
react 子组件暴露,父组件接收
前端·javascript·react.js