1.和为k的子数组
思路分析:利用前缀和和哈希表来高效地解决
1.什么是前缀和?
- 假设 nums = [1, 2, 3, 4],那么前缀和就是从数组开始到每个位置的累加和;
- 对于一个位置 j 的前缀和 s[j],可以表示从第一个元素到 j 位置的总和。这样的话,如果我们想知道从位置 i 到 j 的元素的和,只需要 s[j] - s[i - 1]
2.问题转化为找特定的前缀和差值
- 我们希望找到一个子数组,使得该子数组的和为k。假设当前位置的前缀和为s[j],我们需要寻找一个之前的位置i的前缀和s[i],使得s[j]-s[i] = k,这相当于:s[i] = s[j]- k;
- 因此,如果我们当前的前缀和s[j]-k的值已经出现在之前的前缀和中,那么我们就找到了一个满足为k的子数组。
3.使用哈希表记录前缀和
- 为快速查找s[i]是否出现过,使用一个哈希表s来记录每个前缀和出现的次数
- 遍历数组,计算当前位置的前缀和currentSum;
- 检查currentSum - k 是否已经在哈希表中:
- 如果在,说明从某个之前的位置到当前的位置的子数组和为k,把这个数量累加到结果中;
- 将currentSum的值存入哈希表,以便后续元素使用
- 我们在哈希表中初始化s[0] = 1,这样可以处理前缀和本身等于 k 的情况
具体实现代码(详解版):
cpp
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
// 哈希表用于记录前缀和出现的次数
unordered_map<int, int> s;
s[0] = 1; // 初始化,处理前缀和本身等于 k 的情况
int currentSum = 0;
int res = 0;
for (int num : nums) {
currentSum += num; // 累加当前元素
// 检查是否存在一个前缀和满足 currentSum - k
if (s.find(currentSum - k) != s.end()) {
res += s[currentSum - k];
}
// 更新哈希表中的前缀和次数
s[currentSum]++;
}
return res;
}
};
2.滑动窗口的最大值
思路分析:这个我们可以使用双端队列来高效的解决。双端队列可以帮助我们在每次移动窗口时快速获得最大值。
- 使用双端队列存储候选元素的下标
- 需要一个双端队列dq来存储滑动窗口内看你的最大元素的下标,双端队列中的元素此时始终保持从大到小的顺序。
- 遍历数组,并维护双端队列的状态
- 移除窗口外的元素:当滑动窗口向右移动时,队首元素可能会出滑出窗口范围,因此需要将其移除
- 保持队列元素递减:我们只保留可能成为最大值的元素下标,并且他们按递减顺序排列。对于一个新的元素,我们会依次踏出队尾小于该元素的所有元素,因为这些元素在当前滑动窗口中不可能成为最大值。
- 加入新元素:将当前元素下标加入队尾
- 获取当前窗口的最大值
- 在每次移动窗口后,队首的元素就是当前窗口的最大值。
具体实现代码(详解版):
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;//双端队列,存储下标
vector<int> result;
for(int i = 0 ; i < nums.size() ; i ++){
//移除滑出窗口的元素
if(!dq.empty() && dq.front() == i - k){
dq.pop_front();
}
//移除队列中所有比当前元素小的元素
while(!dq.empty() && nums[dq.back()] < nums[i]){
dq.pop_back();
}
//将当前元素下标加入队列
dq.push_back(i);
//当窗口大小达到k时,将当前窗口的最大值添加到结果
if(i >= k - 1){
result.push_back(nums[dq.front()]);
}
}
return result;
}
};
3.最小覆盖子串
这个题目确实是挺难的(需要花时间去理解和写代码)
思路分析:使用滑动窗口的技巧来解决。具体的思路是用两个指针来表示一个窗口,不断调整窗口的大小和位置,直到找到复合要求的最小窗口。
-
使用两个哈希表记录字符频率
- 用一个哈希表need来存储字符串t中每个字符的需求次数
- 用一个哈希表window来记录当前窗口中包含的每个字符的次数
-
初始化双指针和计数器
- 用左右指针left和right来表示滑动窗口,初始时都指向s的开始位置;
- 用一个变量valid记录当前窗口中满足need条件的字符个数
-
扩大右边界形成有效窗口
- 我们将 right 指针向右移动,扩大窗口
- 每当加入一个字符时,如果该字符是t中需要的,就更新window中的字符数量,且如果该字符的数量达到了need中的要求,就将valid增加1
-
缩小左边界寻找最小窗口
- 当 valid 的值等于 need 中的字符种类数时,表示当前窗口已包含了 t 中所有字符。
- 这时,我们尝试缩小窗口,通过移动 left 指针来去掉窗口左侧的无效字符,直到窗口刚好满足条件。
- 每次找到一个符合条件的窗口时,更新最小子串的起始位置和长度。
-
返回结果
- 最后,返回记录的最小窗口。如果没有找到满足条件的子串,返回空字符串 ""
具体实现代码(详解版):
cpp
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; // 用于记录窗口中满足 need 条件的字符数
int start = 0, minLen = INT_MAX;
while (right < s.size()) {
char c = s[right]; // 即将移入窗口的字符
right++; // 扩大窗口
// 更新 window 中的字符计数
if (need.count(c)) {
window[c]++;
if (window[c] == need[c])
valid++;
}
// 判断左侧窗口是否需要收缩
while (valid == need.size()) {
// 更新最小覆盖子串
if (right - left < minLen) {
start = left;
minLen = right - left;
}
char d = s[left]; // 即将移出窗口的字符
left++; // 收缩窗口
// 更新 window 中的字符计数
if (need.count(d)) {
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
// 返回最小覆盖子串
return minLen == INT_MAX ? "" : s.substr(start, minLen);
}
};
再补充一种解法,思路类似。
cpp
class Solution {
public:
string minWindow(string s, string t) {
string ans = "";
unordered_map<char, int> hash;
for (auto& c : t)
++hash[c]; // 记录 t 中每个字符的需求频率
size_t need = hash.size(); // 记录不同字符的数量
// 双指针初始化
for (int i = 0, j = 0; s[j]; ++j) {
if (--hash[s[j]] == 0) // 若当前字符 s[j] 满足了需求,更新 need
need--;
// 缩小窗口,去除多余字符
while (hash[s[i]] < 0)
++hash[s[i++]]; // 左指针右移,并恢复 hash 中的字符频率
// 更新最小覆盖子串
if (need == 0 && (ans.empty() || ans.size() > j - i + 1))
ans = move(s.substr(i, j - i + 1)); // 更新答案为更短的符合条件的子串
}
return ans;
}
};
后两个题目难度都挺大的,标记一下需要多刷~