1248、统计优美子数组(Medium)
题目描述:给你一个整数数组 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
解释:数列中不包含任何奇数,所以不存在优美子数组。
解题思路:
解法一:暴力枚举
暴力枚举的方法同样可以枚举出所有的子数组,判断每一个子数组中奇数的个数,从而得到结果,时间复杂度最优为O(n^2)
,一般会超出题目的时间限制,不再赘述。
解法二:数学法
因为在这里我们只关心子数组中奇数的个数,对于有多少个偶数并不关心,因此,可以建立一个oddIndex数组,用以依次存储所有奇数的下标索引,那么对于oddIndex数组,其中的每一个元素oddIndex[i]
,都可以作为子数组的一个起点,并且[oddIndex[i],oddIndex[i+k-1]]
就满足正好有k个奇数。
但是,不难想到此时找到的这个子数组头尾都是奇数,所以可以向左右两侧延伸,直到碰到奇数,这样找到的子数组同样是包含k个奇数,因为只是扩展了一些偶数。
而扩展之后的优美子数组个数=(左边的偶数个数+1)*(右边的偶数个数+1),扩展同样基于oddIndex数组就可以方便的找到,这样就可以通过计算得到最终结果。
解法三:前缀和(前缀奇数个数)
这里我们看到的同样是一个子数组的问题,在【前缀和】和为K、和可被K整除的子数组中,我们介绍了涉及到子数组的两类问题,求最值动态规划,不是最值考虑前缀和。那么这里是子数组的个数的问题,和第560、974题非常类似,因此可以在前缀和上做一些考虑。
不难看到,前缀和是前i个元素之和,而这里虽然不是求和,但是我们可以用前缀奇数个数来判断,用pre[i]
表示前i个元素中奇数的个数,那么从 j 到 i 这个子数组的奇数个数可以表示为pre[i]-pre[j-1]
,因此恰好有k个奇数可以表示为:
pre[i]-pre[j-1]==k
这样,实际上就完全转化为了第560题,在遍历到某个数时,找到pre-k
对应的出现次数即可。
解法四:技巧转化
如果我们将数组中的奇数用1表示,偶数用0表示,那么有k个奇数就可以完全转化为和为K的问题。
代码实现:
//解法二:数学法,时间辅助度O(n),空间复杂度O(n)
class Solution {
public int numberOfSubarrays(int[] nums, int k) {
//数学法
if(nums==null || nums.length==0)
return 0;
int res=0,len=nums.length;
int count=0;
int[] oddIndex=new int[len+2];
for(int i=0;i<len;i++){
if(nums[i]%2!=0){ //奇数
oddIndex[++count]=i;
}
}
//添加两个边界值
oddIndex[0]=-1;
oddIndex[++count]=len;
for(int i=1;i+k<=count;i++){
res += (oddIndex[i]-oddIndex[i-1])*(oddIndex[i+k]-oddIndex[i+k-1]);
}
return res;
}
}
//解法三:前缀和(前缀奇数个数),时间辅助度O(n),空间复杂度O(n)
class Solution {
public int numberOfSubarrays(int[] nums, int k) {
//前缀和(前缀奇数个数)+哈希表,遍历过程中统计奇数的个数,pre[i]-pre[j-1]=k
if(nums==null || nums.length==0)
return 0;
Map<Integer,Integer> map=new HashMap<>();
map.put(0,1); //前缀为0个奇数
int pre=0;
int res=0;
for(int i=0;i<nums.length;i++){
if(nums[i]%2!=0) //奇数
pre++;
res+=map.getOrDefault(pre-k,0);
map.put(pre,map.getOrDefault(pre,0)+1);
}
return res;
}
}