560.和为k的子数组
这题的思路是使用前缀和 + 哈希
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n = nums.size();
vector<int> prefixSum(n + 1, 0); // 前缀和数组,多一个元素,prefixSum[0] = 0
unordered_map<int, int> prefixSumCount; // 前缀和出现次数的哈希表
prefixSumCount[0] = 1; // 初始化,前缀和为0出现一次(考虑从数组起始到当前位置的和正好为k的情况)
int count = 0;
// 构建前缀和数组
for (int i = 1; i <= n; i++) {
prefixSum[i] = prefixSum[i - 1] + nums[i - 1];
}
for (int i = 1; i <= n; i++) {
int target = prefixSum[i] - k; // 我们需要找到之前有多少个前缀和等于当前前缀和减去k
if (prefixSumCount.find(target) != prefixSumCount.end()) {
count += prefixSumCount[target]; // 如果存在,增加相应的次数
}
// 更新当前前缀和的出现次数
prefixSumCount[prefixSum[i]]++;
}
return count;
}
};
最开始我的思路是使用前缀和 + 滑动窗口来实现, 但是出现了问题.
因为双指针滑动窗口方法通常适用于所有元素非负的情况,它依赖于数组的累积和单调递增的性质来调整窗口的边界。当数组中包含负数时,累积和不再保证单调性,从而使得双指针方法失效。
例如,对于数组 nums = [-1, -1, 1]
和 k = 0
:
这个数组的累积和会在增加和减少之间变动,使得用双指针法难以控制窗口的大小和位置。
针对这类问题,使用哈希表记录前缀和及其出现次数的方法会更加有效。
这个哈希方法也是很常见的一种方法,相似的题还有两数之和, 四数相加, 可以当模板先记着, 逻辑并不复杂:
当我们找到一个前缀和为 preSum - k
时,说明从之前的某个位置到当前位置的子数组和为 k
。若当前遍历到位置 i
,前缀和为 preSum
,我们要找的是和为 k
的子数组。如果存在一个位置 j
(j < i
),其前缀和为 preSum - k
,那么从位置 j
到位置 i
的子数组和为 k
。
前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和
前缀和的核心思路是我们 new 一个新的数组 preSum
出来,preSum[i]
记录 nums[0..i-1]
的累加和。有这个 preSum
数组,如果我想求索引区间 [1, 4]
内的所有元素之和,就可以通过 preSum[5] - preSum[1]
得出。
这个技巧在生活中运用也挺广泛的,比方说,你们班上有若干同学,每个同学有一个期末考试的成绩(满分 100 分),那么请你实现一个 API,输入任意一个分数段,返回有多少同学的成绩在这个分数段内。
还有一个相似的技巧是差分数组, 差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
比如说,我给你输入一个数组 nums
,然后又要求给区间 nums[2..6]
全部加 1,再给 nums[3..9]
全部减 3,再给 nums[0..4]
全部加 2,再给...
一通操作猛如虎,然后问你,最后 nums
数组的值是什么?
常规的思路很容易,你让我给区间 nums[i..j]
加上 val
,那我就一个 for 循环给它们都加上呗,还能咋样?这种思路的时间复杂度是 O(N),由于这个场景下对 nums
的修改非常频繁,所以效率会很低下。
这里就需要差分数组的技巧,类似前缀和技巧构造的 preSum
数组,我们先对 nums
数组构造一个 diff
差分数组,diff[i]
就是 nums[i]
和 nums[i-1]
之差
通过diff差分数组是可以反推出原始数组nums数组
int diff[nums.size()];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
diff[i] = nums[i] - nums[i - 1];
}
int res[diff.size()];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.size(); i++) {
res[i] = res[i - 1] + diff[i];
}
这样构造差分数组 diff
,就可以快速进行区间增减的操作,如果你想对区间 nums[i..j]
的元素全部加 3,那么只需要让 diff[i] += 3
,然后再让 diff[j+1] -= 3即可.
原理很简单,回想 diff
数组反推 nums
数组的过程,diff[i] += 3
意味着给 nums[i..]
所有的元素都加了 3,然后 diff[j+1] -= 3
又意味着对于 nums[j+1..]
所有元素再减 3,那综合起来,是不是就是对 nums[i..j]
中的所有元素都加 3 了.
只要花费 O(1) 的时间修改 diff
数组,就相当于给 nums
的整个区间做了修改。多次修改 diff
,然后通过 diff
数组反推,即可得到 nums
修改后的结果。
76.最小覆盖子串
这题思路不难,了解滑动窗口过程的, 很容易想到用滑动窗口来解,难点在于维护窗口移动过程中是否找到匹配的所有字符以及个数字符,窗口移动的过程要维护的变量有点多,容易蒙,看了好多题解才看明白.
正常的滑动窗口四步走:
- 进窗口
- 判断
- 出窗口
- 处理结果(这步的位置根据题目要求而变化)
在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的 right 指针,和一个用于「收缩」窗口的 left 指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 s 上滑动窗口,通过移动 right 指针不断扩张窗口。当窗口包含 t 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。
如何判断当前的窗口包含所有 t 所需的字符呢?我们可以用一个哈希表表示 t 中所有的字符以及它们的个数,再用一个哈希表动态维护窗口中所有的字符以及它们的个数,如果这个动态表中包含 t 的哈希表中的所有字符,并且对应的个数都不小于 t 的哈希表中各个字符的个数,那么当前的窗口是「可行」的。
注意:这里 t中可能出现重复的字符,所以我们要记录字符的个数。
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
int start = 0, len = INT_MAX; // 用于记录最小覆盖子串的起始位置和长度
while (right < s.length()) {
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)) {
window[c]++;
if (window[c] == need[c]) {
valid++;
}
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖子串
if (right - left < len) {
start = left;
len = right - left;
}
char d = s[left];
left++;
// 进行窗口内数据的一系列更新
if (need.count(d)) {
if (window[d] == need[d]) {
valid--;
}
window[d]--;
}
}
}
// 返回最小覆盖子串
return len == INT_MAX ? "" : s.substr(start, len);
}
};