除自身以外数组的乘积[中等]

优质博文:IT-BLOG-CN
在这里插入图片描述

一、题目

给你一个整数数组nums,返回数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。题目数据保证数组nums之中任意元素的全部前缀元素和后缀的乘积都在32位整数范围内。请不要使用除法,且在O(n)时间复杂度内完成此题。

示例 1:
输入: nums = [1,2,3,4]
输出: [24,12,8,6]

示例 2:
输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]

2 <= nums.length <= 105
-30 <= nums[i] <= 30
保证数组nums之中任意元素的全部前缀元素和后缀的乘积都在32位整数范围内

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

二、代码

【1】创建左右乘积列表: 我们不能将所有数字的乘积除以给定索引处的数字得到相应的答案,而是利用索引左侧所有数字的乘积和右侧所有数字的乘积(即前缀与后缀)相乘得到答案。初始化两个数组LeftRight,对于指定的下表ileft[i]代表i左侧所有数据的乘积,right[i]代表i右侧所有数据的乘积。我们利用循环将数据填充到lfet[]right[]数组中,然后将left[i]right[i]相乘就是i的左右乘积。

class Solution {
    public int[] productExceptSelf(int[] nums) {
        if (nums == null || nums.length == 0) {
            return null;
        }

        // 我们使用数组,也就是当前数字的left[] 和 right[] 数组,分别存储左右两边的和;
        int len = nums.length;
        int res[] = new int[len];
        int left[] = new int[len];
        int right[] = new int[len];
        // 第一个数之前的数的乘积为1,所以先给个默认值
        left[0] = 1;
        for (int i = 1; i < len; i++) {
            // left 中保存的是i之前所有数的乘积
            left[i] = left[i - 1] * nums[i - 1];
        }
        // 最有边的数也保存为1
        right[len - 1] = 1;
        for (int i = len - 2; i >= 0; i--) {
            right[i] = right[i + 1] * nums[i + 1];
        }

        for (int i = 0; i < len; i++) {
            res[i] = left[i] * right[i];
        }
        return res;
    }
}

时间复杂度: O(N),其中N指的是数组nums的大小。预处理LR数组以及最后的遍历计算都是O(N)的时间复杂度。
空间复杂度: O(N),其中N指的是数组nums的大小。使用了LR数组去构造答案,LR数组的长度为数组nums的大小。

【2】空间复杂度O(1)的方法: 由于输出数组不算在空间复杂度内,那么我们可以将LR数组用输出数组来计算。先把输出数组当作L数组来计算,然后再动态构造R数组得到结果。

class Solution {
    public int[] productExceptSelf(int[] nums) {
        if (nums == null || nums.length == 0) {
            return null;
        }

        // 因为返回的数组可以不算在空间复杂度中,所以可以作为临时变量存放left[]数据
        int len = nums.length;
        int res[] = new int[len];
                // // 第一个数之前的数的乘积为1,所以先给个默认值
        res[0] = 1;
        for (int i = 1; i < len; i++) {
            // left 中保存的是i之前所有数的乘积
            res[i] = res[i - 1] * nums[i - 1];
        }

        // 然后从后向前变量,通过变量 right保存前几位数的乘积
        int right = 1;
        for (int i = len - 1; i >= 0; i--) {
            res[i] *= right;
            // 放在返回值的后面,就相当于i + 1
            right *= nums[i];
        } 
        return res;
    }
}

时间复杂度: O(N),其中N指的是数组nums的大小。分析与方法一相同。
空间复杂度: O(1),输出数组不算进空间复杂度中,因此我们只需要常数的空间存放变量。

【3】本题的难点在于不能使用除法 ,即需要只用乘法生成数组 ans。根据题目对ans[i]的定义,可列出下图所示的表格。

根据表格的主对角线(全为1),可将表格分为上三角下三角两部分。分别迭代计算下三角和上三角两部分的乘积,即可 不使用除法 就获得结果。

下图中A=nums, B=ans
在这里插入图片描述
算法流程:
初始化: 数组ans,其中ans[0]=1;辅助变量tmp=1
计算ans[i]下三角各元素的乘积,直接乘入ans[i]
计算ans[i]上三角各元素的乘积,记为tmp,并乘入ans[i]
返回ans

class Solution {
        //假如nums为[1,2,3,4],那么answer的值分别为[(2,3,4),(1,3,4),(1,2,4),(1,2,3)]
        //如果吧i当前值相乘的时候看做是1那么就有如下样式
        //  1, 2, 3, 4 
        //  1, 1, 3, 4
        //  1, 2, 1, 4
        //  1, 2, 3, 1
        // 他的对角线1将他们分割成了两个三角形,对于answer的元素,
        //我们可以先计算一个三角形每行的乘积,然后再去计算另外一个三角形每行的乘积,
        //然后各行相乘,就是answer每个对应的元素
    public int[] productExceptSelf(int[] nums) {
        int length = nums.length;
        
        //先初始化一个answer数组,但是很多解答都没说明的是这个answer数组,
        //并不是以此计算就得出的结果,而是两次乘积之后的结果
        int[] answer = new int[length];
        //初始化一个初始值,作为三角乘积计算的开始
        answer[0] = 1;
        //先计算左边三角的乘积
        for(int i = 1; i < length; i++){
            answer[i] = answer[i-1] * nums[i-1];
        }
        //再次计算右边三角形,为什么是length-2呢?
        //length-1是最后一个值的索引,但是最后一个值temp[length-1] = 1,
        //也是对应对角线上的1,所以不在进行相乘处理
        //temp的作用是计算右边三角形的乘积的累计值,然后再和answer相乘,
        //注意!!!:不能直接nums[i+1]相乘那会在计算右三角的时候变成每行乘积与nums[i+1]的错误答案
        int temp = 1;
        for(int i = length - 2; i >= 0; i--){
            //先将每行乘积赋予一个中间值
            temp *= nums[i+1];
            answer[i] *= temp;
        }
        return answer;
    }
}

时间复杂度O(N) 其中N为数组长度,两轮遍历数组nums,使用 O(N)时间。
空间复杂度O(1) 变量tmp使用常数大小额外空间(数组ans作为返回值,不计入复杂度考虑)。

原数组:       [1       2       3       4]
左部分的乘积:   1       1      1*2    1*2*3
右部分的乘积: 2*3*4    3*4      4      1
结果:        1*2*3*4  1*3*4   1*2*4  1*2*3*1

从上面的图可以看出,当前位置的结果就是它左部分的乘积再乘以它右部分的乘积。因此需要进行两次遍历,第一次遍历用于求左部分的乘积,第二次遍历在求右部分的乘积的同时,再将最后的计算结果一起求出来。

【4】不准用除法,那我就用右移运算。先求出整个数组的乘积。然后利用极限的思想,用右移运算符,配合加法累算,得出总乘积是数组中每个元素的倍数。

咱们不用除法,但是需要用一点微积分知识。

class Solution {
public:
    int compute(int sum,int val)
    {
        if(val==1) return sum;
        int k=0,cnt;
        long long tmp; 
        while(sum>0)
        {
            cnt=0;
            tmp=val;
            while(tmp<<1<sum)
            {
                cnt++;
                tmp<<=1;
            }
            sum-=tmp;
            k+=1<<cnt;
        }
        return k;
    }
    vector<int> productExceptSelf(vector<int>& nums)
    {
        int n=nums.size(),k;
        int idx=1,cnt=0;
        int sum,val;
        for(auto v:nums)
        {
            if(v==0)
            {
                cnt++;
                if(cnt==2) break;
            }
            else idx*=v;
        }
        if(cnt>0)
        {
            for(auto& v:nums)
            {
                if(cnt==1&&v==0)
                    v=idx;
                else v=0;
            }
            return nums;
        }
        for(auto& v:nums)
        {
            sum=abs(idx);
            val=abs(v);
            k=compute(sum,val);
            if(idx<0&&v<0||idx>0&&v>0)
                v=k;
            else v=k*-1;
        }
        return nums;
    }
};