本文在别人的题解【链接列于参考资料中】基础上加上些许自己的理解,若侵权立删。
问题描述
给你一个整数数组 nums 和一个整数 k。
如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。
请返回这个数组中「优美子数组」的数目。
示例 1:
输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。
解题报告
记录奇数位置
我们发现,如果两个奇数之间(包含自身)一共包含了 k
个奇数,那么这 k
个奇数可以构成的连续子数组个数就是 左边连续的偶数 的个数加 1
乘以 右边连续的偶数 的个数加 1
。
所以 问题的关键就变成如何高效的求每个奇数前 连续的偶数 的个数?
一种解决方案是记录每个奇数所在的位置,那么每个奇数前 连续的偶数 个数为当前奇数的位置减去上一个奇数的位置。
另外在实现的时候需要注意一些细节问题,例如第一个奇数前 连续偶数的个数 如何求?最后一个奇数前 连续偶数的个数 如何求?
解决方案是:在最开始添加上虚拟位置 -1
,在最后添加虚拟位置 n
。
最后遍历所有的 i
, 将第 i
个 1
作为起点,然后累加答案即可。
时间复杂度:
空间复杂度:
滑动窗口
仔细想一想,上面的思想完全可以用滑动窗口来实现,省去记录每个 1
的空间。
- 移动窗口的右边界时,更新窗口内
1
出现的次数; - 当窗口内
1
的数目达到要求的k
时,开始缩收左边界; - 移动左边界时,记录缩收过程遇到的
0
的个数 even,直到遇到1
; - 累加当前的
even
。
时间复杂度:
空间复杂度:
前缀和
这种 做法 和 哈希表优化系列【空间换时间】-Leetcode 560.和为 K 的子数组 有些许相似之处,都用了前缀和。不同的其中一点是:在 Leetcode 560
中,由于前缀和存在负数的可能,所以需要用哈希表来存储前缀和,便于查找,但是此题中,前缀和的范围是可控的而且为正整数,所以我们完全可以将前缀和作为 vector
的索引来存储。
同样都是前缀和,但是具体实现上,我这种菜鸡,还是挺困难的。具体想法和滑动窗口有些相似。
实现代码
记录奇数位置实现
class Solution {
public:
int numberOfSubarrays(vector<int>& nums, int k) {
int n = nums.size();
vector<int> pos;
pos.push_back(-1);
for (int i = 0; i < n; ++i) {
if (nums[i]&1) pos.push_back(i);
}
pos.push_back(n);
int res = 0, sz = pos.size();
for (int i = 1; i+k < sz; ++i) {
res += (pos[i] - pos[i-1]) * (pos[i+k] - pos[i+k-1]);
}
return res;
}
};
滑动窗口实现
class Solution {
public:
int numberOfSubarrays(vector<int>& nums, int k) {
int n = nums.size();
int res = 0, cnt = 0, even = 0;
int l = 0, r = 0;
while (r < n) {
if (cnt < k && (nums[r++]&1)) cnt++;
if (cnt == k) {
even = 1;
while (!(nums[l++]&1)) even++;
cnt--;
}
res += even;
}
return res;
}
};
前缀和+哈希表优化实现
class Solution {
public:
int numberOfSubarrays(vector<int>& nums, int k) {
int n = nums.size();
vector<int> count(n+1, 0);
count[0] = 1;
int res = 0, odd = 0;
for (int i = 0; i < n; ++i) {
odd += nums[i]&1;
if (odd >= k) res += count[odd-k];
count[odd]++;
}
return res;
}
};
参考资料
[1] Leetcode 1248. 统计「优美子数组」
[2] 【每日算法Day 107】面试必考:良心推荐,一题三解,不看后悔一辈子