滑动窗口算法是双指针的一种应用,适用于解决离散结构中具有“连续”性质的最优化问题
以下所有例题均来源于力扣LeetCode
例题一
3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
代码如下:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//注意判空
if(s.size()==0) return 0;
//无需有序
unordered_set<char> temp;
int left = 0, ans = 0;
for(int i = 0; i < s.size(); i++)
{
//消除重复字符
while(temp.find(s[i]) != temp.end())
{
temp.erase(s[left++]);
}
ans = max(ans, i - left + 1);
temp.insert(s[i]);
}
return ans;
}
};
例题二
- 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
代码如下
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int n = nums.size();
if (n == 0) {
return 0;
}
//初始化为无穷大
int ans = INT_MAX;
int start = 0, end = 0;
int sum = 0;
while (end < n) {
sum += nums[end];
//去除重复
while (sum >= s) {
ans = min(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
return ans == INT_MAX ? 0 : ans;
}
};
例题三
- 最小覆盖子串
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
思路:我们在 s 上滑动窗口,通过移动right指针不断扩张窗口。当窗口包含 t 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。
用一个哈希表表示 t 中所有的字符以及它们的个数,用一个哈希表动态维护窗口中所有的字符以及它们的个数,如果这个动态表中包含 t 的哈希表中的所有字符,并且对应的个数都不小于t 的哈希表中各个字符的个数,那么当前的窗口是满足条件的。
代码如下:
class Solution {
public:
//哈希表
unordered_map <char, int> t_map, s_map;
//判断t中的字符是否已经全部包含于窗口中
bool check()
{
for (const auto &p: t_map)
{
if (s_map[p.first] < p.second)
return false;
}
return true;
}
string minWindow(string s, string t)
{
if(s.size() < t.size() || s.size() == 0 || t.size() == 0)
return string();
for (const auto &ch: t)
t_map[ch]++;
//左指针与右指针
int l = 0, r = 0;
int len = INT_MAX, ansL = -1;
//无符号数与有符号数,首先会将有符号数转化为无符号数
//size()获得的是一个无符号数,必须先行转换,否则后续判断会错误
int s_len = s.size();
while (r < s_len)
{
if (t_map.find(s[r]) != t_map.end())
{
s_map[s[r]]++;
}
//尝试缩小窗口
while (check() && l <= r)
{
if (r - l + 1 < len)
{
len = r - l + 1;
ansL = l;
}
if (t_map.find(s[l]) != t_map.end())
{
s_map[s[l]]--;
}
++l;
}
r++;
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};
例题四
滑动窗口的最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
题解一
对于求最大值,联想到的数据结构就是大顶堆,在C++中可以用优先队列来实现。初始时,我们将数组的前 k
个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组中的位置出现在滑动窗口左边界的左侧。
因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除。通过不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组 (num,index)
,即元素num
在数组中的下标为index
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
priority_queue<pair<int, int>> q;
for (int i = 0; i < k; ++i) {
q.emplace(nums[i], i);
}
vector<int> ans = {q.top().first};
for (int i = k; i < n; ++i) {
q.emplace(nums[i], i);
while (q.top().second <= i - k) {
q.pop();
}
ans.push_back(q.top().first);
}
return ans;
}
};
时间复杂度为\(O(nlogn)\),空间复杂度为\(O(n)\)