Leetcode【滑动窗口 双指针】| 1248. 统计「优美子数组」

题目

给你一个整数数组 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

解题

思路(其实是种滑动窗口的思想)

因为优美子数组是元素连续的子数组,所以可以把求解结果当做一个滑动窗口可以变长变短往左往右,如下述例图中红色区域。
分析题目,有以下几种情况

  1. 先判断特殊情况:nums数组里的奇数个数 = 0或<k时,没有优美子数组
  2. nums数组里的奇数个数 = k时, 优美子数组时包含第一个奇数到最后一个奇数的这个连续子数组minArray的所有子数组(因为题目要求是连续的子数组,所以不能跳元素,问题就简单很多), 这个所求的子数组个数res是nums里minArray前面元素的个数leftn+1与后面元素的个数rightn+1的乘积。举个例子,以题目中示例3为例如下图所示:
    示例3
    why res=16? 我们枚举包含minArray的所有nums的连续子数组:
    nums[0]开始符合条件的子数组有4个(数组尾部分别是minArray的最后一个元素和minArray右边的3个元素即rightn+1)(这里就是下述双指针解法中的移动右指针):
    1
    同理,从nums[1]开始,nums[2]开始,…一直到minArray的第一个元素nums[3]开始(这就是leftn+1,就是双指针解法中的移动左指针)的子数组分别也都有4个。
    2
    3
    4
  3. 然后一般情况:nums数组里的奇数个数oddn > k, 首先求出具有k个奇数的所有最小连续子数组minArray(与情况2不同的是这次的minArray不止一个),然后比情况2多一个条件就是,包含minArray的优美子数组是:minArray前面到上一个奇数之间的元素的个数leftn+1 与 minArray后面到下一个奇数之间的元素rightn+1的乘积。
    因为题目要求是连续子数组,所以minArray的情况也没有很复杂,minArray要符合的条件是:
    ① minArray的第一个和最后一个元素一定是奇数
    ② minArray的第一个和最后一个元素的奇数索引差是k-1,表明minArray中有k个奇数。
    比如,oddn = 6, k = 3。minArray的情况就有odd1到odd3,odd2到odd4,odd3到odd5,odd4到odd6这四**(oddn-k+1)**种。

综上分析,我们先求出nums[]中奇数的个数和其索引(用一个数组odd[]来存储所有奇数的索引),就能分析出所有的状况并加以计算。
时间复杂度:O(n),需要遍历整个数组计算出奇数,n为数组nums长度
空间复杂度:O(n),需要额外odd[]数组最大长度为n

java实现

class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        int res = 0;
        int n = nums.length;
        int leftn = 0, rightn = 0;
        //计算nums中奇数数目,并将奇数的索引值存入动态数组
        ArrayList<Integer> odd = new ArrayList<Integer>();
        for(int i = 0; i < n; i++){
            if(nums[i]%2 == 1) odd.add(i);
        }
        int oddn = odd.size();
        //判断特殊情况
        if(oddn == 0) return 0;
        if(oddn == k){
            leftn = odd.get(0);
            rightn = n-1-odd.get(k-1);
            return res = (leftn+1)*(rightn+1);
        }
        //枚举minArray的个数,分别是odd[i]到odd[i+k-1]
        for(int i = 0; i < oddn-k+1;i++){
            if(i == 0){
                leftn = odd.get(i);
                rightn = odd.get(i+k)-odd.get(i+k-1)-1;
            }else if(i == oddn-k){
                leftn = odd.get(i)-odd.get(i-1)-1;
                rightn = n-1-odd.get(i+k-1);
            }else{
                leftn = odd.get(i)-odd.get(i-1)-1;
                rightn = odd.get(i+k)-odd.get(i+k-1)-1;
            }
            res += (leftn+1)*(rightn+1);
        }
        return res;
    }
}

//上述代码简化
class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        int res = 0;
        int n = nums.length;
        int leftn = 0, rightn = 0;
        //计算nums中奇数数目,并将奇数的索引值存入动态数组odd[],并将数组外的-1索引和n索引加入odd的头和尾,以便计算特殊情况(第一个奇数前没有奇数可进行差计算)
        ArrayList<Integer> odd = new ArrayList<Integer>();
        odd.add(-1);
        for(int i = 0; i < n; i++){
            if(nums[i]%2 == 1) odd.add(i);
        }
        odd.add(n);
        int oddn = odd.size()-2;
        //判断特殊情况
        if(oddn == 0) return 0;
        //枚举minArray的个数,分别是odd[i]到odd[i+k-1]
        for(int i = 1; i < 1+oddn-k+1;i++){
            leftn = odd.get(i)-odd.get(i-1)-1;
            rightn = odd.get(i+k)-odd.get(i+k-1)-1;
            res += (leftn+1)*(rightn+1);
        }
        return res;
    }
}

结果

优化

上述题解的优化

优化一:使用静态数组替代动态数组,优化了时间。

class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        int res = 0;
        int n = nums.length;
        int leftn = 0, rightn = 0;
        //计算nums中奇数数目,并将奇数的索引值存入数组odd[],并将数组外的-1索引和n索引加入odd的头和尾,以便计算特殊情况(第一个奇数前没有奇数可进行差计算)
        int[] odd = new int[n+2];//最多n+2个值
        int oddn = 0;
        odd[0] = -1;
        for(int i = 0; i < n; i++){
            if(nums[i]%2 == 1) odd[++oddn]=i;
        }
        odd[oddn+1] = n;
        
        //判断特殊情况
        if(oddn == 0) return 0;
        //枚举minArray的个数,分别是odd[i]到odd[i+k-1]
        for(int i = 1; i < 1+oddn-k+1;i++){
            leftn = odd[i]-odd[i-1]-1;
            rightn = odd[i+k]-odd[i+k-1]-1;
            res += (leftn+1)*(rightn+1);
        }
        return res;
    }
}

静态数组
优化二: 参考力扣题解之一,另一种思路:不存奇数的索引再算奇数之间的偶数元素个数,而是直接将奇数间隔种偶数的元素的个数存起来,避免了存之后再计算的麻烦。优化了时间。

public class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        int preEven=0;
        List<Integer> list=new ArrayList<>();
        //list[i]表示,第i+1个奇数之前的偶数个数,其实就是我们算的leftn,rightn都是一样的
        for(int i:nums){
            if ((i&1)==0){
                preEven++;
            }else{
                list.add(preEven);
                preEven=0;
            }
        }
        list.add(preEven);
        int count=0;
        //list其实就是上一个代码中的oddn+2,oddn为奇数个数
        for (int i=0;i<list.size()-k;i++){
            count += (list.get(i)+1)* (list.get(i+k)+1);
        }
        return count;
    }
}

2

双指针解法

参考Sandy: 双指针,时间O(N),空间O(1)此解法,不需额外空间存储奇偶数。

  1. 初始左右指针都在最左边,维护最小的连续子数组minArray,当minArray的奇数个数cnt达到k时,保存此位置为k_cnt_right_begin。
  2. 然后将右指针r往右移直到指针到数组边界或右边的数字是奇数(cnt+1,其实是计算minArray到下个奇数的rightn值)。
  3. 此时移动左指针l(其实是对leftn的遍历),每次移动对答案的贡献是r- k_cnt_right_begin +1,即左端点是 l,右端点是[k_cnt_right_begin, r][k_cnt_right_begin,r]的区间。移动 l 并更新cnt直到cnt < k。此时再移动右指针重复上述步骤,直到遍历完数组为止。
class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        int n = nums.length;
        
        int l = 0, r = -1, cnt = 0;
        int ans = 0;
        while (r + 1 < n) {
            //cnt += nums[++r] & 1;
            while (r + 1 < n && cnt < k) cnt += nums[++r] & 1;
            if (r >= n) break;
            int k_cnt_right_begin = r;
            while (r + 1 < n && (nums[r + 1] & 1) == 0) ++r;
            while (l <= r && cnt == k) {
                ans += r - k_cnt_right_begin + 1;
                cnt -= nums[l++] & 1;
            }
        }
        
        return ans;
    }
}

4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值