题目
给你一个整数数组 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
解题
思路(其实是种滑动窗口的思想)
因为优美子数组是元素连续的子数组,所以可以把求解结果当做一个滑动窗口可以变长变短往左往右,如下述例图中红色区域。
分析题目,有以下几种情况
- 先判断特殊情况:nums数组里的奇数个数 = 0或<k时,没有优美子数组
- nums数组里的奇数个数 = k时, 优美子数组时包含第一个奇数到最后一个奇数的这个连续子数组minArray的所有子数组(因为题目要求是连续的子数组,所以不能跳元素,问题就简单很多), 这个所求的子数组个数res是nums里minArray前面元素的个数leftn+1与后面元素的个数rightn+1的乘积。举个例子,以题目中示例3为例如下图所示:
why res=16? 我们枚举包含minArray的所有nums的连续子数组:
从nums[0]开始符合条件的子数组有4个(数组尾部分别是minArray的最后一个元素和minArray右边的3个元素即rightn+1)(这里就是下述双指针解法中的移动右指针):
同理,从nums[1]开始,nums[2]开始,…一直到minArray的第一个元素nums[3]开始(这就是leftn+1,就是双指针解法中的移动左指针)的子数组分别也都有4个。
- 然后一般情况: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;
}
}
双指针解法
参考Sandy: 双指针,时间O(N),空间O(1)此解法,不需额外空间存储奇偶数。
- 初始左右指针都在最左边,维护最小的连续子数组minArray,当minArray的奇数个数cnt达到k时,保存此位置为k_cnt_right_begin。
- 然后将右指针r往右移直到指针到数组边界或右边的数字是奇数(cnt+1,其实是计算minArray到下个奇数的rightn值)。
- 此时移动左指针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;
}
}