238. 除自身以外数组的乘积

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

示例:

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

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

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

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

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/product-of-array-except-self 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

题解 :

  • 这道题看起来非常容易,直接把数组中所有元素的乘积算出来
  • 计算某个位置的值时,直接用总的乘积 / 当前元素的值即可得到最终结果
  • 但是题目中有明确要求,不得使用除法
  • 所以想线性时间复杂度的方法不太容易想

思路一 : 暴力法

  • 暴力法很容易想到
  • 当计算第 i 位的值时,取出 除第 i 位以外的元素,计算乘积
  • 此算法毫无疑问是正确的,但是时间复杂度很高,存在大量冗余的计算
  • 提交后,超时了…
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 *
 * 暴力法
 * 暴力法很容易想到
 * 当计算 i 位置的值时,取出 除i位置以外的元素,计算乘积
 *
 * 时间复杂度 : O(N ^ 2)
 * 空间复杂度 : O(1)
 *
 * */
public int[] constructArr(int[] a) {
    if (a == null || a.length == 0) return a;

    int[] res = new int[a.length];
    for (int i = 0; i < a.length; i++) {
        int cur = 1;
        for (int j = 0; j < a.length; j++) {
            if (i == j) continue;

            cur += a[j];
        }
        res[i] = cur;
    }
    return res;
}

复杂度分析 :

时间复杂度 : O(N ^ 2) 两遍暴力循环

空间复杂度 : O(1) 没有使用额外的存储空间

截屏2020-06-28下午6.42.47

思路二: 线性时间复杂度的优化

  • 在一个评论中看到,这道题可以看作 从前往后 和 从后往前的两遍 dp,深以为然

    截屏2020-06-28下午7.27.33

  • 分别计算出某元素,前边所有元素的积后边元素的积

  • 两者的成绩,也就是要求的除自身以外的所有元素的乘积

  • 我们使用 prev数组 存储 i 元素 前边所有元素的乘积

  • 使用 next数组 存储 i 元素。后边所有元素的乘积

  • 最终结果数组 res[i] = prev[i] * next[i]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 *
 * 分别计算出某元素 前边所有元素的积 和 后边所有元素的积
 * 两者的乘积,就是要求的除自身以外的子数组的乘积
 *
 * 我们使用 prev数组 存储 i 元素 前边所有元素的乘积
 * next数组 存储 i 元素 后边所有元素的乘积
 *
 * 最终的结果数组 res[i] = prev[i] * next[i]
 *
 * 时间复杂度 : O(N)
 * 空间复杂度 : O(N)
 *
 * */
public int[] constructArr1(int[] a) {

    if (a == null || a.length == 0) return a;

    int[] prev = new int[a.length];
    prev[0] = 1;
    for (int i = 1; i < a.length; i++) {
        prev[i] = a[i - 1] * prev[i - 1];
    }

    int[] next = new int[a.length + 1];
    next[a.length] = 1;
    for (int i = a.length - 1; i >= 0; i--) {
        next[i] = a[i + 1] * next[i + 1];
    }

    int[] res = new int[a.length];
    for (int i = 0; i < prev.length; i++) {
        res[i] = prev[i] * next[i];
    }
    return res;
}

复杂度分析 :

时间复杂度 : O(N) 线性遍历数组

空间复杂度 : O(N) 使用了额外的 prev 和 next数组存储空间

相比较第一种暴力法来说,这种解法在执行效率上就很优秀了。

截屏2020-06-28下午6.03.46

思路三: 空间复杂度的优化

  • 思路二,已经很好的解决了问题。 但是仍然有优化的空间
    • 1,空间复杂度
      • 题目进阶中有要求,是否可以使用 O(1) 空间复杂度解决问题?
    • 2,循环次数
      • 思路二,虽然是O(N)的时间复杂度,但是我们将数组遍历了 3 遍
      • 是否可以减少遍历数组的次数?
  • 下边这种解法,我们尝试进行上述两点的优化
    • 初始化一个 res 数组,用于存储最终答案
    • 在计算元素前边元素乘积时,将结果存入 res 数组
    • 在计算元素后边元素乘积时,同步将结果存入 res 数组中
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 *
 * 优化
 * 1,空间复杂度
 * 题目中进阶有要求
 * 是否可以 O(1) 空间复杂度解决问题?
 *
 * 2, 循环次数
 * 上述解法,我们遍历了3遍数组
 * 是否可以少遍历一遍数组?
 *
 * 下边这种解法,我们尝试进行上述两点的优化
 *
 * [1,2,3,4,5]
 * [120,60,40,30,24]
 *
 * [1,1,2,6,24]
 * [,30,24]
 * */
public int[] constructArr12int2(int[] a) {

    if (a == null || a.length == 0) return a;

    int[] res = new int[a.length];
    res[0] = 1;
    for (int i = 1; i < a.length; i++) {
        res[i] = a[i - 1] * res[i - 1];
    }

    int R = 1;
    for (int i = a.length - 1; i >= 0; i--) {
        res[i] = res[i - 1] * R;
        R *= res[i];
    }
    return res;
}

复杂度分析 :

时间复杂度 : O(N) 遍历了两遍数组, 时间复杂度 : O(N)

空间复杂度 : O(1) 题目中说,答案数组不被算作额外的存储空间,所以此解法,空间复杂度 O(1)

截屏2020-06-28下午7.15.16