1248. 统计「优美子数组」

此题同 560, 974解题思路一致,复习时,可以一块看。 标签前序和。一并看懂前序和的问题。

给你一个整数数组 nums 和一个整数 k。

如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。

请返回这个数组中「优美子数组」的数目。

示例 1:

输入:nums = [1,1,2,1,1], k = 3 输出:2 解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。 示例 2:

输入:nums = [2,4,6], k = 1 输出:0 解释:数列中不包含任何奇数,所以不存在优美子数组。 示例 3:

输入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2 输出:16

提示:

1 <= nums.length <= 50000 1 <= nums[i] <= 10^5 1 <= k <= nums.length

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

题解:
思路一:
  • 暴力解法:容易想到,但是时间复杂度高

  • 记录前序和数组 arr (数组中存放,前 i 为奇数的个数)

  • 双重循环 记录arr[j] - arrp[i] == k 的个数

  • 时间复杂度 : O(N ^2) 其中N为数组中元素的个数

  • 空间复杂度: O(N) 使用了额外的arr数组空间,保存N个元素

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    // 计算前缀和数组 arr
    // 双重循环 arr[j] - arr[i] == k 的个数, 时间复杂度 : O(N ^ 2)
    // leetcode提交后,华丽丽的超时了..
    public static int numberOfSubarrays1(int[] nums, int k) {

        int[] prefix = new int[nums.length + 1];
        prefix[0] = 0;
        for (int i = 1; i <= nums.length; i++) {
            int num = nums[i - 1];
            boolean isodd = num % 2 == 1;
            prefix[i] = i == 0 ? (isodd ? 1 : 0) : (isodd ? prefix[i - 1] + 1 : prefix[i - 1]);
        }

        int count = 0;
        for (int i = 0; i < prefix.length; i++) {
            for (int j = i + 1; j < prefix.length; j++) {
                if (prefix[j] - prefix[i]== k) count ++;
            }
        }
        return count;
    }

然而提交后,由于时间复杂度过高。 超时了…

屏幕快照 2020-05-27 下午4.30.23

思路二:

优化

  • 使用 map 将时间复杂度 优化至 O(N)
  • map中存放 <前缀和(也就是奇数的个数, 前缀和的个数)>
  • prev变量,数组从0到当前遍历的元素,奇数的个数
  • count变量,记录i j组合的个数
  • 当prev >= k 时, 如果 map 中的key包含 prev - k , 则 count += map.get(prev - k)
  • 给当前元素赋值,之前有,则 + 1. 没有赋值为1
  • 时间复杂度 : O(N)
  • 空间复杂度 : O(N)

代码如下:

 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
// 优化
    // 使用 map 将时间复杂度 优化值 O(N)
    // map中存放 <前缀和(也就是奇数的个数), 前缀和的个数>
    // prev变量,记录当前遍历的元素前,奇数的个数
    // count变量,记录i j组合的个数
    // 当prev >= k 时, 如果 map 中包含 prev - k. 则 count += map.get(perv - k)
    // 并且,map中重新赋值
    // 此算法时间复杂度 O(N)
    // 空间复杂度 O(N)
    public static int numberOfSubarrays2(int[] nums, int k) {

        // <前缀和,前缀和的个数>
        HashMap<Integer,Integer> map = new HashMap<>();
        map.put(0,1);

        int prev = 0, count = 0;
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            prev += num & 1;

            if (prev >= k) count += map.get(prev - k);

            map.put(prev,map.getOrDefault(prev,0) + 1);
        }

        return count;
    }

屏幕快照 2020-05-27 下午5.01.54

思路三:

优化

  • 使用数组代替map
  • 思路一样,只是将map 改为 数组
  • hashmap在存取值时, 需要进行hash运算。 并且map也需要比数组更大的存储空间。
  • 所以可以用数组,代替map。 当可以使用数组实现某种算法时,比hashmap更优秀。

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    // 优化
    // map 优化为 数组
    // 思路一样, 只是将 map 改为 数组
    // hashmap在存取值时,需要进行hash运算,也需要更大的存储空间
    // 所以可以用数组实现算法时,比hashmap更优秀
    public static int numberOfSubarrays(int[] nums, int k) {

        int[] prefixs = new int[nums.length + 1];
        prefixs[0] = 1;

        int prev = 0, count = 0;
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            prev += num & 1;

            if (prev >= k) count += prefixs[prev - k];

            prefixs[prev] = prefixs[prev] + 1;
        }

        return count;
    }

image-20200527172227709

思路四:

滑动窗口

  • 不断右移 right 指针来扩大窗口,使其包含 k 个奇数
  • 当滑动窗口包含了k个奇数时,则计算 当前窗口优美子数组个数
    • 统计第 1 个奇数左边的偶数个数 leftCnt。 这leftCnt个偶数都可以作为当前窗口优美子数组的起点,起点的选择有 leftCnt + 1种。 因为一个都不包含,从奇数元素开始。
    • 统计第 k 个奇数右边的偶数个数 rightCnt, 直到碰到下一个奇数,或者遍历完数组位置。终点的选择有rightCnt + 1种。 因为可以一个都不包含,到第k个奇数为止。
  • 时间复杂度 : O(N)
  • 空间复杂度 : O(1)
 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
37
38
39
40
41
// 滑动窗口
    public static int numberOfSubarrays(int[] nums, int k) {
        int left = 0, right = 0, res = 0, oddCnt = 0;

        while (right < nums.length){
            if ((nums[right ++] & 1) == 1){
                oddCnt ++;
            }

            // 若当前滑动窗口 [left right) 中有k个奇数了,进入判断统计优美子数组个数。
            if (oddCnt == k){
                // 先将滑动窗口的右边届向右扩展,直到遇到下一个奇数或者出界
                // rightCnt 为第k个奇数 右边偶数的个数
                int tmp = right;
                while (right < nums.length && (nums[right] & 1) == 0){
                    right ++;
                }
                int rightCnt = right - tmp;

                // leftCnt 为第 1 个奇数左边的偶数的个数
                int leftCnt = 0;
                while ((nums[left] & 1) == 0){
                    leftCnt ++;
                    left ++;
                }

                // 第 1 个奇数左边的 leftCnt 个偶数都可以作为 优美子数组的起点
                // 因为 第 1 个奇数可以1个偶数都不取,所以起点的选择有 leftCnt + 1种
                // 第 k 个奇数右边 rightCnt 个偶数都可以作为优美子数组的终点
                // 因为 第 k 个奇数右边可以1个偶数都不取, 所以终点的选择有 rightCnt + 1种
                // 所以该滑动窗口中, 优美子数组 左右起点的选择组合数为 (leftCnt + 1) * (rightCnt + 1)
                res += (leftCnt + 1) * (rightCnt + 1);

                // 此时 left指向的是第一个奇数,因为该区间统计完了,因此left 右移一位, oddCnt --
                left ++;
                oddCnt --;
            }
        }

        return res;
    }

屏幕快照 2020-05-27 下午6.14.11