前端刷题路-Day86:除自身以外数组的乘积(题号238)

432 阅读4分钟

这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战

除自身以外数组的乘积(题号238)

题目

给你一个长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。

示例:

输入: [1,2,3,4]
输出: [24,12,8,6]

**提示:**题目数据保证数组之中任意元素的全部前缀元素和后缀(甚至是整个数组)的乘积都在 32 位整数范围内。

**说明: 请不要使用除法,**且在 O(n) 时间复杂度内完成此题。

进阶: 你可以在常数空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)

链接

leetcode-cn.com/problems/pr…

解释

这题啊,这题是DP变种。

说实话,看到这题的第一眼,感觉用两层循环就可以解决了。

首先第一层循环,循环每个元素,内层循环就是计算整个数组种除了当前数字的乘积,在JavaScript可以使用reduce来简化代码,但这样的时间复杂度就是O(n * n)了,显然不符合题目的说明。

后续也证明了这样的做法确实没法AC,因为在测试用例靠后的位置会有个巨长的数组来让你超时。

那除了这种做法还有什么方法呢?想了10分钟后,发现DP可以解决这个问题。

dp[i][j]可以代表一个数组中从ij位置上所有数字的乘积,那么要求一个除一个数字以外的其它数字乘积就很简单了,举个例子:

[1,2,3,4]

比方说现在取2个这个位置上的其它数字的乘积,它的index1,那么假设已经有DP数组了,那么当前位置的乘积就是:

dp[0][0] * dp[2][3]

dp[0][0]代表着1dp[2][3]代表着3 * 4,两个一乘就是1 * 3 * 4,也就是我们需要的结果了。

那怎么拿到这个DP数组呢?在求之前我们需要知道这个DP数组应该包含怎样的数据。

因为如果是平常的DP数组,拿到数组的方法肯定是两个for循环,这样的时间复杂度就已经是O(n * n)了,已经超出了题目的要求,所以需要简化一下这个DP数组。

回到刚才的问题上?这个DP数组需要为我们提供怎样的数据?

因为都是从头到尾的计算,所以它只需要有两层:

  • 第一层表示从数组的第一个元素到每个元素位置的乘积

    也就是dp[i]表示从0开始到i位置的所有数字的乘积

  • 第二层表示从数组的最后一个元素到每个元素位置的乘积

    也就是dp[i]表示从nums.length - 1i位置的所有数字乘积,也就是反向的第一层数据

了解这两层后就好办了,可以循环数组两次,分别求除DP数组的两层,也可以只循环一层,利用nums.lengthi的关系可以在一个for循环中求出两层数组的值。

拿到值之后再遍历下每个元素,就可以拿到最后的结果了。

自己的答案(双for循环)

var productExceptSelf = function(nums) {
  const len = nums.length
  const res = new Array(len)
  for (let i = 0; i < len; i++) {
    res[i] = nums.reduce((total, num, index) => {
      if (index === i) return total
      return total * num
    }, 1)
  }
  return res
};

代码很简单,就双层for循环呗,也很简单的失败了,倒在了19/20个测试用例。

自己的答案(DP+双数组)

这就是解释中说的方法了👇:

var productExceptSelf = function(nums) {
  const len = nums.length
  const dp = Array.from({length: 2}, () => new Array())
  const res = new Array(len)
  dp[0][0] = 1
  dp[1][0] = 1
  for (let i = 1; i <= len; i++) {
    dp[0].push(dp[0][i - 1] * nums[i - 1])
    dp[1].push(dp[1][i - 1] * nums[len - i])
  }
  for (let i = 1; i <= len; i++) {
    res[i - 1] = dp[0][i - 1] * dp[1][len - i] 
  }
  return res
};

这里有几点需要注意的地方:

  1. 初始化DP数组时没有给数组长度

    因为下面用的push方法,此处给长度没啥意义,给了长度也可以,不过就没必要用push了,都可以

  2. DP数组的初始化赋值

    这里的DP数组是比nums数组长一位的,Why?因为第一个元素和最后一个元素的问题,如果是求第一个元素的值,那么显然dp[0][i - 1]是不存在的,此时DP公式就会出现问题,这里在前面多放一个1,就是为了防止取不到值的问题,dp[1]作为反向数组,自然也是要做一样的操作,否则也会出现取不到值的问题

在成功拿到DP数组后,最后for循环一次,就能拿到最后的res了,清晰明了

更好的方法(DP+单数组)

👆的解法虽然AC了,单依然不符合题意,因为空间复杂度是O(n)的,毕竟有DP数组的存在。

那如何优化到O(1)呢?注意题目说到res其实不算O(n),因为毕竟是返回值。

那就得在res上做文章了,是不是可以这么做

  • res初始化赋值的时候赋值的是上面dp[0]的数组
  • 之后利用一个变量来累计dp[1]数组的变化,依次给res重新赋值

这样就节省掉了DP双数组的空间,让答案符合O(1)的空间复杂度。

var productExceptSelf = function(nums) {
  const len = nums.length
  const res = new Array(len)
  res[0] = 1
  for (let i = 1; i < len; i++) {
    res[i] = res[i - 1] * nums[i - 1]    
  }
  let count = 1
  for (let i = len - 1; i >= 0; i--) {
    res[i] = res[i] * count
    count *= nums[i]    
  }
  return res
};

整体解法就是这样,第一个for循环用来给res初始化赋值,之后累计count变量的值,依次赋值给res中的元素,这样两次for循环之后,res就可以拿到最后的结果了,还是比较巧妙的。



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)